diff --git a/docs/GENERATORS.md b/docs/GENERATORS.md index f1c07dc22..78f8942b9 100644 --- a/docs/GENERATORS.md +++ b/docs/GENERATORS.md @@ -15,12 +15,10 @@ To generate a practice exercise's tests, the test generator: ### Step 1: read `canonical-data.json` file -The test generator parses the test cases from the exercise's `canonical-data.json` using the [JSON.net library](https://www.newtonsoft.com/json). +The test generator parses the test cases from the exercise's `canonical-data.json` using the [System.Text.Json namespace](https://learn.microsoft.com/en-us/dotnet/api/system.text.json). Since some canonical data uses nesting, the parsed test case includes an additional `path` field that contains the `description` properties of any parent elements, as well as the test case's own `description` property. -Note: the JSON is parsed to an `ExpandoObject` instance, which makes dealing with dynamic data easier. - ### Step 2: omit excluded tests from `tests.toml` file Each exercise has a `tests.toml` file, in which individual tests can be excluded/disabled. @@ -44,7 +42,7 @@ Finally, the output of the rendered template is written to the exercise's test f ## Templates -The templates are rendered using the [Handlebars.Net library](https://github.com/Handlebars-Net/Handlebars.Net), which supports [handlebars syntax](https://handlebarsjs.com/). +The templates are rendered using the [Scriban library](https://github.com/scriban/scriban/). ## Command-line interface diff --git a/exercises/practice/acronym/.meta/Generator.tpl b/exercises/practice/acronym/.meta/Generator.tpl index 730b28e88..ef94a6fa5 100644 --- a/exercises/practice/acronym/.meta/Generator.tpl +++ b/exercises/practice/acronym/.meta/Generator.tpl @@ -2,11 +2,11 @@ using Xunit; public class AcronymTests { - {{#test_cases}} - [Fact{{#unless @first}}(Skip = "Remove this Skip property to run this test"){{/unless}}] - public void {{test_method_name}}() + {{for testCase in testCases}} + [Fact{{if !for.first}}(Skip = "Remove this Skip property to run this test"){{end}}] + public void {{testCase.testMethodName}}() { - Assert.Equal({{lit expected}}, Acronym.Abbreviate({{lit input.phrase}})); + Assert.Equal({{testCase.expected | string.literal}}, Acronym.Abbreviate({{testCase.input.phrase | string.literal}})); } - {{/test_cases}} + {{end}} } diff --git a/exercises/practice/affine-cipher/.meta/Generator.tpl b/exercises/practice/affine-cipher/.meta/Generator.tpl index 015794d23..50419cafb 100644 --- a/exercises/practice/affine-cipher/.meta/Generator.tpl +++ b/exercises/practice/affine-cipher/.meta/Generator.tpl @@ -3,27 +3,15 @@ using Xunit; public class AffineCipherTests { - {{#test_cases_by_property.encode}} - [Fact{{#unless @first}}(Skip = "Remove this Skip property to run this test"){{/unless}}] - public void {{short_test_method_name}}() + {{for testCase in testCases}} + [Fact{{if !for.first}}(Skip = "Remove this Skip property to run this test"){{end}}] + public void {{testCase.shortTestMethodName}}() { - {{#if expected.error}} - Assert.Throws(() => AffineCipher.Encode({{lit input.phrase}}, {{input.key.a}}, {{input.key.b}})); + {{if testCase.expected.error}} + Assert.Throws(() => AffineCipher.{{testCase.property | string.capitalize}}({{testCase.input.phrase | string.literal}}, {{testCase.input.key.a}}, {{testCase.input.key.b}})); {{else}} - Assert.Equal({{lit expected}}, AffineCipher.Encode({{lit input.phrase}}, {{input.key.a}}, {{input.key.b}})); - {{/if}} + Assert.Equal({{testCase.expected | string.literal}}, AffineCipher.{{testCase.property | string.capitalize}}({{testCase.input.phrase | string.literal}}, {{testCase.input.key.a}}, {{testCase.input.key.b}})); + {{end}} } - {{/test_cases_by_property.encode}} - - {{#test_cases_by_property.decode}} - [Fact(Skip = "Remove this Skip property to run this test")] - public void {{short_test_method_name}}() - { - {{#if expected.error}} - Assert.Throws(() => AffineCipher.Decode({{lit input.phrase}}, {{input.key.a}}, {{input.key.b}})); - {{else}} - Assert.Equal({{lit expected}}, AffineCipher.Decode({{lit input.phrase}}, {{input.key.a}}, {{input.key.b}})); - {{/if}} - } - {{/test_cases_by_property.decode}} + {{end}} } diff --git a/exercises/practice/allergies/.meta/Generator.tpl b/exercises/practice/allergies/.meta/Generator.tpl index 05e3926cb..b75533a51 100644 --- a/exercises/practice/allergies/.meta/Generator.tpl +++ b/exercises/practice/allergies/.meta/Generator.tpl @@ -2,26 +2,30 @@ using Xunit; public class AllergiesTests { - {{#test_cases_by_property.allergicTo}} - [Fact{{#unless @first}}(Skip = "Remove this Skip property to run this test"){{/unless}}] - public void {{test_method_name}}() + {{for testCase in testCasesByProperty.allergicTo}} + [Fact{{if !for.first}}(Skip = "Remove this Skip property to run this test"){{end}}] + public void {{testCase.testMethodName}}() { - var sut = new Allergies({{input.score}}); - Assert.{{expected}}(sut.IsAllergicTo(Allergen.{{Capitalize input.item}})); + var sut = new Allergies({{testCase.input.score}}); + Assert.{{testCase.expected ? "True" : "False"}}(sut.IsAllergicTo({{testCase.input.item | enum "Allergen"}})); } - {{/test_cases_by_property.allergicTo}} + {{end}} - {{#test_cases_by_property.list}} + {{for testCase in testCasesByProperty.list}} [Fact(Skip = "Remove this Skip property to run this test")] - public void {{test_method_name}}() + public void {{testCase.testMethodName}}() { - var sut = new Allergies({{input.score}}); - {{#isempty expected}} + var sut = new Allergies({{testCase.input.score}}); + {{if testCase.expected.empty?}} Assert.Empty(sut.List()); {{else}} - Allergen[] expected = [{{#each ../expected}}Allergen.{{Capitalize .}}{{#unless @last}},{{/unless}}{{/each}}]; + Allergen[] expected = [ + {{for expected in testCase.expected}} + {{expected | enum "Allergen"}}{{if !for.last}},{{end}} + {{end}} + ]; Assert.Equal(expected, sut.List()); - {{/isempty}} + {{end}} } - {{/test_cases_by_property.list}} + {{end}} } diff --git a/exercises/practice/darts/.meta/Generator.tpl b/exercises/practice/darts/.meta/Generator.tpl index c9f120542..86a338750 100644 --- a/exercises/practice/darts/.meta/Generator.tpl +++ b/exercises/practice/darts/.meta/Generator.tpl @@ -2,11 +2,11 @@ using Xunit; public class DartsTests { - {{#test_cases}} - [Fact{{#unless @first}}(Skip = "Remove this Skip property to run this test"){{/unless}}] - public void {{test_method_name}}() + {{for testCase in testCases}} + [Fact{{if !for.first}}(Skip = "Remove this Skip property to run this test"){{end}}] + public void {{testCase.testMethodName}}() { - Assert.Equal({{expected}}, Darts.Score({{input.x}}, {{input.y}})); + Assert.Equal({{testCase.expected}}, Darts.Score({{testCase.input.x}}, {{testCase.input.y}})); } - {{/test_cases}} + {{end}} } diff --git a/exercises/practice/darts/DartsTests.cs b/exercises/practice/darts/DartsTests.cs index bf3d3a143..b771f9f84 100644 --- a/exercises/practice/darts/DartsTests.cs +++ b/exercises/practice/darts/DartsTests.cs @@ -65,7 +65,7 @@ public void Just_outside_the_middle_circle() [Fact(Skip = "Remove this Skip property to run this test")] public void Just_within_the_outer_circle() { - Assert.Equal(1, Darts.Score(-7, 7)); + Assert.Equal(1, Darts.Score(-7.0, 7.0)); } [Fact(Skip = "Remove this Skip property to run this test")] diff --git a/exercises/practice/difference-of-squares/.meta/Generator.tpl b/exercises/practice/difference-of-squares/.meta/Generator.tpl index db36cd758..377babb10 100644 --- a/exercises/practice/difference-of-squares/.meta/Generator.tpl +++ b/exercises/practice/difference-of-squares/.meta/Generator.tpl @@ -2,27 +2,11 @@ using Xunit; public class DifferenceOfSquaresTests { - {{#test_cases_by_property.squareOfSum}} - [Fact{{#unless @first}}(Skip = "Remove this Skip property to run this test"){{/unless}}] - public void {{short_test_method_name}}() + {{for testCase in testCases}} + [Fact{{if !for.first}}(Skip = "Remove this Skip property to run this test"){{end}}] + public void {{testCase.shortTestMethodName}}() { - Assert.Equal({{expected}}, DifferenceOfSquares.CalculateSquareOfSum({{input.number}})); + Assert.Equal({{testCase.expected}}, DifferenceOfSquares.Calculate{{testCase.property | string.capitalize}}({{testCase.input.number}})); } - {{/test_cases_by_property.squareOfSum}} - - {{#test_cases_by_property.sumOfSquares}} - [Fact(Skip = "Remove this Skip property to run this test")] - public void {{short_test_method_name}}() - { - Assert.Equal({{expected}}, DifferenceOfSquares.CalculateSumOfSquares({{input.number}})); - } - {{/test_cases_by_property.sumOfSquares}} - - {{#test_cases_by_property.differenceOfSquares}} - [Fact(Skip = "Remove this Skip property to run this test")] - public void {{short_test_method_name}}() - { - Assert.Equal({{expected}}, DifferenceOfSquares.CalculateDifferenceOfSquares({{input.number}})); - } - {{/test_cases_by_property.differenceOfSquares}} + {{end}} } diff --git a/exercises/practice/eliuds-eggs/.meta/Generator.tpl b/exercises/practice/eliuds-eggs/.meta/Generator.tpl index 9e91728bd..327111bd8 100644 --- a/exercises/practice/eliuds-eggs/.meta/Generator.tpl +++ b/exercises/practice/eliuds-eggs/.meta/Generator.tpl @@ -2,11 +2,11 @@ using Xunit; public class EliudsEggsTests { - {{#test_cases}} - [Fact{{#unless @first}}(Skip = "Remove this Skip property to run this test"){{/unless}}] - public void {{test_method_name}}() + {{for testCase in testCases}} + [Fact{{if !for.first}}(Skip = "Remove this Skip property to run this test"){{end}}] + public void {{testCase.testMethodName}}() { - Assert.Equal({{expected}}, EliudsEggs.EggCount({{input.number}})); + Assert.Equal({{testCase.expected}}, EliudsEggs.EggCount({{testCase.input.number}})); } - {{/test_cases}} + {{end}} } diff --git a/exercises/practice/hamming/.meta/Generator.tpl b/exercises/practice/hamming/.meta/Generator.tpl index 1d9ea9c19..65b12ce3a 100644 --- a/exercises/practice/hamming/.meta/Generator.tpl +++ b/exercises/practice/hamming/.meta/Generator.tpl @@ -3,15 +3,15 @@ using Xunit; public class HammingTests { - {{#test_cases}} - [Fact{{#unless @first}}(Skip = "Remove this Skip property to run this test"){{/unless}}] - public void {{test_method_name}}() + {{for testCase in testCases}} + [Fact{{if !for.first}}(Skip = "Remove this Skip property to run this test"){{end}}] + public void {{testCase.testMethodName}}() { - {{#if expected.error}} - Assert.Throws(() => Hamming.Distance({{lit input.strand1}}, {{lit input.strand2}})); + {{if testCase.expected.error}} + Assert.Throws(() => Hamming.Distance({{testCase.input.strand1 | string.literal}}, {{testCase.input.strand2 | string.literal}})); {{else}} - Assert.Equal({{expected}}, Hamming.Distance({{lit input.strand1}}, {{lit input.strand2}})); - {{/if}} + Assert.Equal({{testCase.expected}}, Hamming.Distance({{testCase.input.strand1 | string.literal}}, {{testCase.input.strand2 | string.literal}})); + {{end}} } - {{/test_cases}} + {{end}} } diff --git a/exercises/practice/isogram/.meta/Generator.tpl b/exercises/practice/isogram/.meta/Generator.tpl index f34414ce4..1e25d7bd6 100644 --- a/exercises/practice/isogram/.meta/Generator.tpl +++ b/exercises/practice/isogram/.meta/Generator.tpl @@ -2,11 +2,11 @@ using Xunit; public class IsogramTests { - {{#test_cases}} - [Fact{{#unless @first}}(Skip = "Remove this Skip property to run this test"){{/unless}}] - public void {{test_method_name}}() + {{for testCase in testCases}} + [Fact{{if !for.first}}(Skip = "Remove this Skip property to run this test"){{end}}] + public void {{testCase.testMethodName}}() { - Assert.{{expected}}(Isogram.IsIsogram({{lit input.phrase}})); + Assert.{{testCase.expected ? "True" : "False"}}(Isogram.IsIsogram({{testCase.input.phrase | string.literal}})); } - {{/test_cases}} + {{end}} } diff --git a/exercises/practice/leap/.meta/Generator.tpl b/exercises/practice/leap/.meta/Generator.tpl index 7eedba9f3..4a2da0ee3 100644 --- a/exercises/practice/leap/.meta/Generator.tpl +++ b/exercises/practice/leap/.meta/Generator.tpl @@ -2,11 +2,11 @@ using Xunit; public class LeapTests { - {{#test_cases}} - [Fact{{#unless @first}}(Skip = "Remove this Skip property to run this test"){{/unless}}] - public void {{test_method_name}}() + {{for testCase in testCases}} + [Fact{{if !for.first}}(Skip = "Remove this Skip property to run this test"){{end}}] + public void {{testCase.testMethodName}}() { - Assert.{{expected}}(Leap.IsLeapYear({{input.year}})); + Assert.{{testCase.expected ? "True" : "False"}}(Leap.IsLeapYear({{testCase.input.year}})); } - {{/test_cases}} + {{end}} } diff --git a/exercises/practice/pangram/.meta/Generator.tpl b/exercises/practice/pangram/.meta/Generator.tpl index 8e28209cd..d940c0d15 100644 --- a/exercises/practice/pangram/.meta/Generator.tpl +++ b/exercises/practice/pangram/.meta/Generator.tpl @@ -2,11 +2,11 @@ using Xunit; public class PangramTests { - {{#test_cases}} - [Fact{{#unless @first}}(Skip = "Remove this Skip property to run this test"){{/unless}}] - public void {{test_method_name}}() + {{for testCase in testCases}} + [Fact{{if !for.first}}(Skip = "Remove this Skip property to run this test"){{end}}] + public void {{testCase.testMethodName}}() { - Assert.{{lit expected}}(Pangram.IsPangram({{lit input.sentence}})); + Assert.{{testCase.expected ? "True" : "False"}}(Pangram.IsPangram({{testCase.input.sentence | string.literal}})); } - {{/test_cases}} + {{end}} } diff --git a/exercises/practice/perfect-numbers/.meta/Generator.tpl b/exercises/practice/perfect-numbers/.meta/Generator.tpl index 88523e2d7..832c1fd75 100644 --- a/exercises/practice/perfect-numbers/.meta/Generator.tpl +++ b/exercises/practice/perfect-numbers/.meta/Generator.tpl @@ -3,15 +3,15 @@ using Xunit; public class PerfectNumbersTests { - {{#test_cases}} - [Fact{{#unless @first}}(Skip = "Remove this Skip property to run this test"){{/unless}}] - public void {{short_test_method_name}}() + {{for testCase in testCases}} + [Fact{{if !for.first}}(Skip = "Remove this Skip property to run this test"){{end}}] + public void {{testCase.shortTestMethodName}}() { - {{#if expected.error}} - Assert.Throws(() => PerfectNumbers.Classify({{input.number}})); + {{if testCase.expected.error}} + Assert.Throws(() => PerfectNumbers.Classify({{testCase.input.number}})); {{else}} - Assert.Equal(Classification.{{Capitalize expected}}, PerfectNumbers.Classify({{input.number}})); - {{/if}} + Assert.Equal({{testCase.expected | enum "Classification"}}, PerfectNumbers.Classify({{testCase.input.number}})); + {{end}} } - {{/test_cases}} + {{end}} } diff --git a/exercises/practice/rotational-cipher/.meta/Generator.tpl b/exercises/practice/rotational-cipher/.meta/Generator.tpl index acef16e3f..2f203a4d6 100644 --- a/exercises/practice/rotational-cipher/.meta/Generator.tpl +++ b/exercises/practice/rotational-cipher/.meta/Generator.tpl @@ -2,11 +2,11 @@ using Xunit; public class RotationalCipherTests { - {{#test_cases}} - [Fact{{#unless @first}}(Skip = "Remove this Skip property to run this test"){{/unless}}] - public void {{test_method_name}}() + {{for testCase in testCases}} + [Fact{{if !for.first}}(Skip = "Remove this Skip property to run this test"){{end}}] + public void {{testCase.testMethodName}}() { - Assert.Equal({{lit expected}}, RotationalCipher.Rotate({{lit input.text}}, {{input.shiftKey}})); + Assert.Equal({{testCase.expected | string.literal}}, RotationalCipher.Rotate({{testCase.input.text | string.literal}}, {{testCase.input.shiftKey}})); } - {{/test_cases}} + {{end}} } diff --git a/exercises/practice/sieve/.meta/Generator.tpl b/exercises/practice/sieve/.meta/Generator.tpl index b25b8ce50..39d304890 100644 --- a/exercises/practice/sieve/.meta/Generator.tpl +++ b/exercises/practice/sieve/.meta/Generator.tpl @@ -2,12 +2,12 @@ using Xunit; public class SieveTests { - {{#test_cases}} - [Fact{{#unless @first}}(Skip = "Remove this Skip property to run this test"){{/unless}}] - public void {{test_method_name}}() + {{for testCase in testCases}} + [Fact{{if !for.first}}(Skip = "Remove this Skip property to run this test"){{end}}] + public void {{testCase.testMethodName}}() { - int[] expected = [{{expected}}]; - Assert.Equal(expected, Sieve.Primes({{input.limit}})); + int[] expected = {{testCase.expected}}; + Assert.Equal(expected, Sieve.Primes({{testCase.input.limit}})); } - {{/test_cases}} + {{end}} } diff --git a/exercises/practice/space-age/.meta/Generator.tpl b/exercises/practice/space-age/.meta/Generator.tpl index b39b16717..576d573c6 100644 --- a/exercises/practice/space-age/.meta/Generator.tpl +++ b/exercises/practice/space-age/.meta/Generator.tpl @@ -2,12 +2,12 @@ using Xunit; public class SpaceAgeTests { - {{#test_cases}} - [Fact{{#unless @first}}(Skip = "Remove this Skip property to run this test"){{/unless}}] - public void {{test_method_name}}() + {{for testCase in testCases}} + [Fact{{if !for.first}}(Skip = "Remove this Skip property to run this test"){{end}}] + public void {{testCase.testMethodName}}() { - var sut = new SpaceAge({{input.seconds}}); - Assert.Equal({{lit expected}}, sut.On{{input.planet}}(), precision: 2); + var sut = new SpaceAge({{testCase.input.seconds}}); + Assert.Equal({{testCase.expected}}, sut.On{{testCase.input.planet}}(), precision: 2); } - {{/test_cases}} + {{end}} } diff --git a/exercises/practice/square-root/.meta/Generator.tpl b/exercises/practice/square-root/.meta/Generator.tpl index 6f416ef87..1525a300b 100644 --- a/exercises/practice/square-root/.meta/Generator.tpl +++ b/exercises/practice/square-root/.meta/Generator.tpl @@ -2,11 +2,11 @@ using Xunit; public class SquareRootTests { - {{#test_cases}} - [Fact{{#unless @first}}(Skip = "Remove this Skip property to run this test"){{/unless}}] - public void {{test_method_name}}() + {{for testCase in testCases}} + [Fact{{if !for.first}}(Skip = "Remove this Skip property to run this test"){{end}}] + public void {{testCase.testMethodName}}() { - Assert.Equal({{expected}}, SquareRoot.Root({{input.radicand}})); + Assert.Equal({{testCase.expected}}, SquareRoot.Root({{testCase.input.radicand}})); } - {{/test_cases}} + {{end}} } diff --git a/exercises/practice/strain/.meta/Generator.tpl b/exercises/practice/strain/.meta/Generator.tpl index e7a463aea..e10d2b3be 100644 --- a/exercises/practice/strain/.meta/Generator.tpl +++ b/exercises/practice/strain/.meta/Generator.tpl @@ -2,49 +2,31 @@ using System.Collections.Generic; using System.Linq; using Xunit; +{{func normalize_predicate}} + {{ $0 | string.replace "fn(x) ->" "x => " | + string.replace "starts_with(x, " "x.StartsWith(" | + string.replace "contains(x, " "x.Contains(" }} +{{end}} + +{{func test_case_type}} + {{if $0 | string.contains "strings"}} + string[] + {{else if $0 | string.contains "lists"}} + int[][] + {{else}} + int[] + {{end}} +{{end}} + public class StrainTests { - {{#test_cases_by_property.keep}} - [Fact{{#unless @first}}(Skip = "Remove this Skip property to run this test"){{/unless}}] - public void {{test_method_name}}() - { - {{#equals description "keeps lists"}} - int[][] expected = [{{../expected}}]; - int[][] input = [{{#../input.list}}[{{.}}]{{#unless @last}}, {{/unless}}{{/../input.list}}]; - Assert.Equal(expected, input.Keep(x => x.Contains(5)).ToArray()); - {{else}} - {{#equals ../description "keeps strings"}} - string[] expected = [{{#../../expected}}{{lit .}}{{#unless @last}}, {{/unless}}{{/../../expected}}]; - string[] input = [{{#../../input.list}}{{lit .}}{{#unless @last}}, {{/unless}}{{/../../input.list}}]; - Assert.Equal(expected, input.Keep(x => x.StartsWith('z')).ToArray()); - {{else}} - int[] expected = [{{../../expected}}]; - int[] input = [{{../../input.list}}]; - Assert.Equal(expected, input.Keep(x => {{replace ../../input.predicate "fn(x) -> " ""}}).ToArray()); - {{/equals}} - {{/equals}} - } - {{/test_cases_by_property.keep}} - - {{#test_cases_by_property.discard}} - [Fact(Skip = "Remove this Skip property to run this test")] - public void {{test_method_name}}() + {{for testCase in testCases}} + [Fact{{if !for.first}}(Skip = "Remove this Skip property to run this test"){{end}}] + public void {{testCase.testMethodName}}() { - {{#equals description "discards lists"}} - int[][] expected = [{{../expected}}]; - int[][] input = [{{#../input.list}}[{{.}}]{{#unless @last}}, {{/unless}}{{/../input.list}}]; - Assert.Equal(expected, input.Discard(x => x.Contains(5)).ToArray()); - {{else}} - {{#equals ../description "discards strings"}} - string[] expected = [{{#../../expected}}{{lit .}}{{#unless @last}}, {{/unless}}{{/../../expected}}]; - string[] input = [{{#../../input.list}}{{lit .}}{{#unless @last}}, {{/unless}}{{/../../input.list}}]; - Assert.Equal(expected, input.Discard(x => x.StartsWith('z')).ToArray()); - {{else}} - int[] expected = [{{../../expected}}]; - int[] input = [{{../../input.list}}]; - Assert.Equal(expected, input.Discard(x => {{replace ../../input.predicate "fn(x) -> " ""}}).ToArray()); - {{/equals}} - {{/equals}} + {{testCase.description | test_case_type}} expected = {{testCase.expected}}; + {{testCase.description | test_case_type}} input = {{testCase.input.list}}; + Assert.Equal(expected, input.{{testCase.property | string.capitalize}}({{testCase.input.predicate | normalize_predicate}}).ToArray()); } - {{/test_cases_by_property.discard}} + {{end}} } diff --git a/exercises/practice/sublist/.meta/Generator.tpl b/exercises/practice/sublist/.meta/Generator.tpl index 34a2f2c59..a866e930b 100644 --- a/exercises/practice/sublist/.meta/Generator.tpl +++ b/exercises/practice/sublist/.meta/Generator.tpl @@ -3,11 +3,13 @@ using Xunit; public class SublistTests { - {{#test_cases}} - [Fact{{#unless @first}}(Skip = "Remove this Skip property to run this test"){{/unless}}] - public void {{test_method_name}}() + {{for testCase in testCases}} + [Fact{{if !for.first}}(Skip = "Remove this Skip property to run this test"){{end}}] + public void {{testCase.testMethodName}}() { - Assert.Equal(SublistType.{{capitalize expected}}, Sublist.Classify(new List{{#isempty input.listOne}}(){{else}} { {{../input.listOne}} }{{/isempty}}, new List{{#isempty input.listTwo}}(){{else}} { {{../input.listTwo}} }{{/isempty}})); + List listOne = {{testCase.input.listOne}}; + List listTwo = {{testCase.input.listTwo}}; + Assert.Equal({{testCase.expected | enum "SublistType"}}, Sublist.Classify(listOne, listTwo)); } - {{/test_cases}} + {{end}} } diff --git a/exercises/practice/sublist/SublistTests.cs b/exercises/practice/sublist/SublistTests.cs index 9a66d91a1..133b46a66 100644 --- a/exercises/practice/sublist/SublistTests.cs +++ b/exercises/practice/sublist/SublistTests.cs @@ -6,108 +6,144 @@ public class SublistTests [Fact] public void Empty_lists() { - Assert.Equal(SublistType.Equal, Sublist.Classify(new List(), new List())); + List listOne = []; + List listTwo = []; + Assert.Equal(SublistType.Equal, Sublist.Classify(listOne, listTwo)); } [Fact(Skip = "Remove this Skip property to run this test")] public void Empty_list_within_non_empty_list() { - Assert.Equal(SublistType.Sublist, Sublist.Classify(new List(), new List { 1, 2, 3 })); + List listOne = []; + List listTwo = [1, 2, 3]; + Assert.Equal(SublistType.Sublist, Sublist.Classify(listOne, listTwo)); } [Fact(Skip = "Remove this Skip property to run this test")] public void Non_empty_list_contains_empty_list() { - Assert.Equal(SublistType.Superlist, Sublist.Classify(new List { 1, 2, 3 }, new List())); + List listOne = [1, 2, 3]; + List listTwo = []; + Assert.Equal(SublistType.Superlist, Sublist.Classify(listOne, listTwo)); } [Fact(Skip = "Remove this Skip property to run this test")] public void List_equals_itself() { - Assert.Equal(SublistType.Equal, Sublist.Classify(new List { 1, 2, 3 }, new List { 1, 2, 3 })); + List listOne = [1, 2, 3]; + List listTwo = [1, 2, 3]; + Assert.Equal(SublistType.Equal, Sublist.Classify(listOne, listTwo)); } [Fact(Skip = "Remove this Skip property to run this test")] public void Different_lists() { - Assert.Equal(SublistType.Unequal, Sublist.Classify(new List { 1, 2, 3 }, new List { 2, 3, 4 })); + List listOne = [1, 2, 3]; + List listTwo = [2, 3, 4]; + Assert.Equal(SublistType.Unequal, Sublist.Classify(listOne, listTwo)); } [Fact(Skip = "Remove this Skip property to run this test")] public void False_start() { - Assert.Equal(SublistType.Sublist, Sublist.Classify(new List { 1, 2, 5 }, new List { 0, 1, 2, 3, 1, 2, 5, 6 })); + List listOne = [1, 2, 5]; + List listTwo = [0, 1, 2, 3, 1, 2, 5, 6]; + Assert.Equal(SublistType.Sublist, Sublist.Classify(listOne, listTwo)); } [Fact(Skip = "Remove this Skip property to run this test")] public void Consecutive() { - Assert.Equal(SublistType.Sublist, Sublist.Classify(new List { 1, 1, 2 }, new List { 0, 1, 1, 1, 2, 1, 2 })); + List listOne = [1, 1, 2]; + List listTwo = [0, 1, 1, 1, 2, 1, 2]; + Assert.Equal(SublistType.Sublist, Sublist.Classify(listOne, listTwo)); } [Fact(Skip = "Remove this Skip property to run this test")] public void Sublist_at_start() { - Assert.Equal(SublistType.Sublist, Sublist.Classify(new List { 0, 1, 2 }, new List { 0, 1, 2, 3, 4, 5 })); + List listOne = [0, 1, 2]; + List listTwo = [0, 1, 2, 3, 4, 5]; + Assert.Equal(SublistType.Sublist, Sublist.Classify(listOne, listTwo)); } [Fact(Skip = "Remove this Skip property to run this test")] public void Sublist_in_middle() { - Assert.Equal(SublistType.Sublist, Sublist.Classify(new List { 2, 3, 4 }, new List { 0, 1, 2, 3, 4, 5 })); + List listOne = [2, 3, 4]; + List listTwo = [0, 1, 2, 3, 4, 5]; + Assert.Equal(SublistType.Sublist, Sublist.Classify(listOne, listTwo)); } [Fact(Skip = "Remove this Skip property to run this test")] public void Sublist_at_end() { - Assert.Equal(SublistType.Sublist, Sublist.Classify(new List { 3, 4, 5 }, new List { 0, 1, 2, 3, 4, 5 })); + List listOne = [3, 4, 5]; + List listTwo = [0, 1, 2, 3, 4, 5]; + Assert.Equal(SublistType.Sublist, Sublist.Classify(listOne, listTwo)); } [Fact(Skip = "Remove this Skip property to run this test")] public void At_start_of_superlist() { - Assert.Equal(SublistType.Superlist, Sublist.Classify(new List { 0, 1, 2, 3, 4, 5 }, new List { 0, 1, 2 })); + List listOne = [0, 1, 2, 3, 4, 5]; + List listTwo = [0, 1, 2]; + Assert.Equal(SublistType.Superlist, Sublist.Classify(listOne, listTwo)); } [Fact(Skip = "Remove this Skip property to run this test")] public void In_middle_of_superlist() { - Assert.Equal(SublistType.Superlist, Sublist.Classify(new List { 0, 1, 2, 3, 4, 5 }, new List { 2, 3 })); + List listOne = [0, 1, 2, 3, 4, 5]; + List listTwo = [2, 3]; + Assert.Equal(SublistType.Superlist, Sublist.Classify(listOne, listTwo)); } [Fact(Skip = "Remove this Skip property to run this test")] public void At_end_of_superlist() { - Assert.Equal(SublistType.Superlist, Sublist.Classify(new List { 0, 1, 2, 3, 4, 5 }, new List { 3, 4, 5 })); + List listOne = [0, 1, 2, 3, 4, 5]; + List listTwo = [3, 4, 5]; + Assert.Equal(SublistType.Superlist, Sublist.Classify(listOne, listTwo)); } [Fact(Skip = "Remove this Skip property to run this test")] public void First_list_missing_element_from_second_list() { - Assert.Equal(SublistType.Unequal, Sublist.Classify(new List { 1, 3 }, new List { 1, 2, 3 })); + List listOne = [1, 3]; + List listTwo = [1, 2, 3]; + Assert.Equal(SublistType.Unequal, Sublist.Classify(listOne, listTwo)); } [Fact(Skip = "Remove this Skip property to run this test")] public void Second_list_missing_element_from_first_list() { - Assert.Equal(SublistType.Unequal, Sublist.Classify(new List { 1, 2, 3 }, new List { 1, 3 })); + List listOne = [1, 2, 3]; + List listTwo = [1, 3]; + Assert.Equal(SublistType.Unequal, Sublist.Classify(listOne, listTwo)); } [Fact(Skip = "Remove this Skip property to run this test")] public void First_list_missing_additional_digits_from_second_list() { - Assert.Equal(SublistType.Unequal, Sublist.Classify(new List { 1, 2 }, new List { 1, 22 })); + List listOne = [1, 2]; + List listTwo = [1, 22]; + Assert.Equal(SublistType.Unequal, Sublist.Classify(listOne, listTwo)); } [Fact(Skip = "Remove this Skip property to run this test")] public void Order_matters_to_a_list() { - Assert.Equal(SublistType.Unequal, Sublist.Classify(new List { 1, 2, 3 }, new List { 3, 2, 1 })); + List listOne = [1, 2, 3]; + List listTwo = [3, 2, 1]; + Assert.Equal(SublistType.Unequal, Sublist.Classify(listOne, listTwo)); } [Fact(Skip = "Remove this Skip property to run this test")] public void Same_digits_but_different_numbers() { - Assert.Equal(SublistType.Unequal, Sublist.Classify(new List { 1, 0, 1 }, new List { 10, 1 })); + List listOne = [1, 0, 1]; + List listTwo = [10, 1]; + Assert.Equal(SublistType.Unequal, Sublist.Classify(listOne, listTwo)); } } diff --git a/exercises/practice/sum-of-multiples/.meta/Generator.tpl b/exercises/practice/sum-of-multiples/.meta/Generator.tpl index 92b0f2074..78e97fe59 100644 --- a/exercises/practice/sum-of-multiples/.meta/Generator.tpl +++ b/exercises/practice/sum-of-multiples/.meta/Generator.tpl @@ -2,11 +2,11 @@ using Xunit; public class SumOfMultiplesTests { - {{#test_cases}} - [Fact{{#unless @first}}(Skip = "Remove this Skip property to run this test"){{/unless}}] - public void {{test_method_name}}() + {{for testCase in testCases}} + [Fact{{if !for.first}}(Skip = "Remove this Skip property to run this test"){{end}}] + public void {{testCase.testMethodName}}() { - Assert.Equal({{expected}}, SumOfMultiples.Sum([{{input.factors}}], {{input.limit}})); + Assert.Equal({{testCase.expected}}, SumOfMultiples.Sum({{testCase.input.factors}}, {{testCase.input.limit}})); } - {{/test_cases}} + {{end}} } diff --git a/exercises/practice/triangle/.meta/Generator.tpl b/exercises/practice/triangle/.meta/Generator.tpl index c2aa48a04..adbcc15c0 100644 --- a/exercises/practice/triangle/.meta/Generator.tpl +++ b/exercises/practice/triangle/.meta/Generator.tpl @@ -2,27 +2,27 @@ using Xunit; public class TriangleTests { - {{#test_cases_by_property.equilateral}} - [Fact{{#unless @first}}(Skip = "Remove this Skip property to run this test"){{/unless}}] - public void {{test_method_name}}() + {{for testCase in testCasesByProperty.equilateral}} + [Fact{{if !for.first}}(Skip = "Remove this Skip property to run this test"){{end}}] + public void {{testCase.testMethodName}}() { - Assert.{{expected}}(Triangle.IsEquilateral({{input.sides.0}}, {{input.sides.1}}, {{input.sides.2}})); + Assert.{{testCase.expected ? "True" : "False"}}(Triangle.IsEquilateral({{testCase.input.sides | array.join ", "}})); } - {{/test_cases_by_property.equilateral}} + {{end}} - {{#test_cases_by_property.isosceles}} + {{for testCase in testCasesByProperty.isosceles}} [Fact(Skip = "Remove this Skip property to run this test")] - public void {{test_method_name}}() + public void {{testCase.testMethodName}}() { - Assert.{{expected}}(Triangle.IsIsosceles({{input.sides.0}}, {{input.sides.1}}, {{input.sides.2}})); + Assert.{{testCase.expected ? "True" : "False"}}(Triangle.IsIsosceles({{testCase.input.sides | array.join ", "}})); } - {{/test_cases_by_property.isosceles}} + {{end}} - {{#test_cases_by_property.scalene}} + {{for testCase in testCasesByProperty.scalene}} [Fact(Skip = "Remove this Skip property to run this test")] - public void {{test_method_name}}() + public void {{testCase.testMethodName}}() { - Assert.{{expected}}(Triangle.IsScalene({{input.sides.0}}, {{input.sides.1}}, {{input.sides.2}})); + Assert.{{testCase.expected ? "True" : "False"}}(Triangle.IsScalene({{testCase.input.sides | array.join ", "}})); } - {{/test_cases_by_property.scalene}} + {{end}} } diff --git a/exercises/practice/two-fer/.meta/Generator.tpl b/exercises/practice/two-fer/.meta/Generator.tpl index 0eaa2c515..b578efd7c 100644 --- a/exercises/practice/two-fer/.meta/Generator.tpl +++ b/exercises/practice/two-fer/.meta/Generator.tpl @@ -2,11 +2,11 @@ using Xunit; public class TwoFerTests { - {{#test_cases}} - [Fact{{#unless @first}}(Skip = "Remove this Skip property to run this test"){{/unless}}] - public void {{test_method_name}}() + {{for testCase in testCases}} + [Fact{{if !for.first}}(Skip = "Remove this Skip property to run this test"){{end}}] + public void {{testCase.testMethodName}}() { - Assert.Equal({{lit expected}}, TwoFer.Speak({{#if input.name}}{{lit input.name}}{{/if}})); + Assert.Equal({{testCase.expected | string.literal}}, TwoFer.Speak({{if !testCase.input.name.empty?}}{{testCase.input.name | string.literal}}{{end}})); } - {{/test_cases}} + {{end}} } diff --git a/exercises/practice/wordy/.meta/Generator.tpl b/exercises/practice/wordy/.meta/Generator.tpl index dc097d805..842cdfd16 100644 --- a/exercises/practice/wordy/.meta/Generator.tpl +++ b/exercises/practice/wordy/.meta/Generator.tpl @@ -3,15 +3,15 @@ using Xunit; public class WordyTests { - {{#test_cases}} - [Fact{{#unless @first}}(Skip = "Remove this Skip property to run this test"){{/unless}}] - public void {{test_method_name}}() + {{for testCase in testCases}} + [Fact{{if !for.first}}(Skip = "Remove this Skip property to run this test"){{end}}] + public void {{testCase.testMethodName}}() { - {{#if expected.error}} - Assert.Throws(() => Wordy.Answer({{lit input.question}})); + {{if testCase.expected.error}} + Assert.Throws(() => Wordy.Answer({{testCase.input.question | string.literal}})); {{else}} - Assert.Equal({{expected}}, Wordy.Answer({{lit input.question}})); - {{/if}} + Assert.Equal({{testCase.expected}}, Wordy.Answer({{testCase.input.question | string.literal}})); + {{end}} } - {{/test_cases}} + {{end}} } diff --git a/exercises/practice/zebra-puzzle/.meta/Generator.tpl b/exercises/practice/zebra-puzzle/.meta/Generator.tpl index f2154670b..07d459424 100644 --- a/exercises/practice/zebra-puzzle/.meta/Generator.tpl +++ b/exercises/practice/zebra-puzzle/.meta/Generator.tpl @@ -2,11 +2,11 @@ using Xunit; public class ZebraPuzzleTests { - {{#test_cases}} - [Fact{{#unless @first}}(Skip = "Remove this Skip property to run this test"){{/unless}}] - public void {{test_method_name}}() + {{for testCase in testCases}} + [Fact{{if !for.first}}(Skip = "Remove this Skip property to run this test"){{end}}] + public void {{testCase.testMethodName}}() { - Assert.Equal(Nationality.{{expected}}, ZebraPuzzle.{{Capitalize property}}()); + Assert.Equal(Nationality.{{testCase.expected}}, {{testCase.property | enum "ZebraPuzzle"}}()); } - {{/test_cases}} + {{end}} } diff --git a/generators/CanonicalData.cs b/generators/CanonicalData.cs index 7dbc0045f..82dc68571 100644 --- a/generators/CanonicalData.cs +++ b/generators/CanonicalData.cs @@ -1,12 +1,12 @@ using System.Collections.Immutable; +using System.Text.Json; +using System.Text.Json.Nodes; using LibGit2Sharp; -using Newtonsoft.Json.Linq; - namespace Generators; -internal record CanonicalData(Exercise Exercise, JObject[] TestCases); +internal record CanonicalData(Exercise Exercise, JsonNode[] TestCases); internal static class CanonicalDataParser { @@ -14,26 +14,26 @@ internal static class CanonicalDataParser internal static CanonicalData Parse(Exercise exercise) => new(exercise, ParseTestCases(exercise)); - private static JObject[] ParseTestCases(Exercise exercise) - { - var jsonObject = JObject.Parse(File.ReadAllText(Paths.CanonicalDataFile(exercise))); - return ParseTestCases(jsonObject, ImmutableQueue.Empty).ToArray(); - } + private static JsonNode[] ParseTestCases(Exercise exercise) => + ParseTestCases(ParseCanonicalData(exercise), ImmutableQueue.Empty).ToArray(); + + private static JsonNode ParseCanonicalData(Exercise exercise) => + JsonNode.Parse(File.ReadAllText(Paths.CanonicalDataFile(exercise)))!; - private static IEnumerable ParseTestCases(JObject jsonObject, ImmutableQueue path) + private static IEnumerable ParseTestCases(JsonNode jsonNode, ImmutableQueue path) { - var updatedPath = jsonObject.TryGetValue("description", out var description) - ? path.Enqueue(description.Value()!) + var updatedPath = jsonNode["description"] is {} description + ? path.Enqueue(description.GetValue()) : path; - return jsonObject.TryGetValue("cases", out var cases) - ? ((JArray)cases).Cast().SelectMany(child => ParseTestCases(child, updatedPath)) - : [ToTestCase(jsonObject, updatedPath)]; + return jsonNode["cases"] is {} cases + ? cases.AsArray().SelectMany(child => ParseTestCases(child!, updatedPath)) + : [ToTestCase(jsonNode, updatedPath)]; } - private static JObject ToTestCase(JObject testCaseJson, IEnumerable path) + private static JsonNode ToTestCase(JsonNode testCaseJson, IEnumerable path) { - testCaseJson["path"] = JArray.FromObject(path); + testCaseJson["path"] = JsonSerializer.SerializeToNode(path); return testCaseJson; } diff --git a/generators/Generators.csproj b/generators/Generators.csproj index 93a764cec..320c71925 100644 --- a/generators/Generators.csproj +++ b/generators/Generators.csproj @@ -9,13 +9,10 @@ - - - - + diff --git a/generators/Naming.cs b/generators/Naming.cs index d00369c68..e27556d8e 100644 --- a/generators/Naming.cs +++ b/generators/Naming.cs @@ -4,9 +4,8 @@ namespace Generators; internal static class Naming { - internal static string ToMethodName(params object[] path) => - path.Cast() - .Unwords() + internal static string ToMethodName(params string[] path) => + path.Unwords() .Words() .Select(Transform) .Unwords() diff --git a/generators/Templates.cs b/generators/Templates.cs index 57da9b2fa..a59a382d4 100644 --- a/generators/Templates.cs +++ b/generators/Templates.cs @@ -1,72 +1,49 @@ -using System.Dynamic; -using System.Globalization; +using System.Text.Json; +using System.Text.Json.Nodes; -using HandlebarsDotNet; -using HandlebarsDotNet.Helpers; +using Humanizer; -using Newtonsoft.Json.Linq; +using Scriban; +using Scriban.Runtime; namespace Generators; internal static class Templates { - private static readonly IHandlebars HandlebarsContext = Handlebars.Create(); - - static Templates() + public static string RenderTestsCode(CanonicalData canonicalData) { - HandlebarsHelpers.Register(HandlebarsContext, options => { options.UseCategoryPrefix = false; }); - HandlebarsContext.Configuration.FormatProvider = CultureInfo.InvariantCulture; - - HandlebarsContext.RegisterHelper("lit", (writer, context, parameters) => - writer.WriteSafeString(Formatting.FormatLiteral(parameters.First()))); + var scriptObject = new ScriptObject(); + scriptObject.Import("enum", new Func((text, enumType) => $"{enumType.Pascalize()}.{text.Pascalize()}"));; + scriptObject.Import(TemplateData.ForCanonicalData(canonicalData)); - HandlebarsContext.RegisterHelper("equals", (output, options, context, arguments) => - { - if (arguments.Length != 2) throw new HandlebarsException("{{#equals}} helper must have exactly two arguments"); + var context = new TemplateContext(); + context.PushGlobal(scriptObject); - if (arguments[0]!.Equals(arguments[1]!)) - options.Template(output, context); - else - options.Inverse(output, context); - }); + return Template.Parse(File.ReadAllText(Paths.TemplateFile(canonicalData.Exercise))) + .Render(context); } - public static string RenderTestsCode(CanonicalData canonicalData) => - CompileTemplate(canonicalData.Exercise)(TemplateData.ForCanonicalData(canonicalData)); - - private static HandlebarsTemplate CompileTemplate(Exercise exercise) => - HandlebarsContext.Compile(File.ReadAllText(Paths.TemplateFile(exercise))); - private static class TemplateData { - internal static Dictionary ForCanonicalData(CanonicalData canonicalData) + internal static JsonElement ForCanonicalData(CanonicalData canonicalData) { var testCases = canonicalData.TestCases.Select(Create).ToArray(); + var testCasesByProperty = GroupTestCasesByProperty(testCases); - return new() - { - ["test_cases"] = testCases.ToArray(), - ["test_cases_by_property"] = GroupTestCasesByProperty(testCases) - }; + return JsonSerializer.SerializeToElement(new { testCases, testCasesByProperty }); } - private static ExpandoObject Create(JToken testCase) + private static JsonElement Create(JsonNode testCase) { - dynamic testData = testCase.ToObject()!; - testData.test_method_name = Naming.ToMethodName(testData.path.ToArray()); - testData.short_test_method_name = Naming.ToMethodName(testData.description); - - if (testCase["expected"] is JArray expected) - { - testData.expected = expected.Select(e => e.ToString()).ToArray(); - } + testCase["testMethodName"] = Naming.ToMethodName(testCase["path"]!.AsArray().GetValues().ToArray()); + testCase["shortTestMethodName"] = Naming.ToMethodName(testCase["description"]!.GetValue()); - return testData; + return JsonSerializer.SerializeToElement(testCase); } - private static Dictionary GroupTestCasesByProperty(IEnumerable testCases) => + private static Dictionary GroupTestCasesByProperty(IEnumerable testCases) => testCases - .GroupBy(testCase => (string)testCase.property) + .GroupBy(testCase => testCase.GetProperty("property").GetString()!) .ToDictionary(kv => kv.Key, kv => kv.ToArray()); } -} +} \ No newline at end of file diff --git a/generators/TestCasesConfiguration.cs b/generators/TestCasesConfiguration.cs index 21b367058..fce9896b7 100644 --- a/generators/TestCasesConfiguration.cs +++ b/generators/TestCasesConfiguration.cs @@ -9,7 +9,7 @@ internal static CanonicalData RemoveExcludedTestCases(CanonicalData canonicalDat { var excludedTestCaseIds = ExcludedTestCaseIds(canonicalData.Exercise); var includedTestCases = canonicalData.TestCases - .Where(testCase => !excludedTestCaseIds.Contains(testCase["uuid"]!.ToObject()!)) + .Where(testCase => !excludedTestCaseIds.Contains(testCase["uuid"]!.GetValue())) .ToArray(); return canonicalData with { TestCases = includedTestCases };