Skip to content

Commit e3142cb

Browse files
Generators refactoring (#601)
* generators: Ignore Rider settings files * generators: Refactor additional namespaces * exercises: Update to latest canonical data * exercises: Use Array.Empty<T> instead of new T[0] * generators: Use static class * generators: Remove unused using statements * generators: Move exercise field to canonical data case class * generators: Use environment specific newline * generators: Fix culture-specific output * generators: Replace UpdateCanonicalData with UpdateCanonicalDataCase * generators: Test method body rendering methods return IEnumerable<string> * generators: Use var everywhere * generator: Code cleanup * generators: Remove unused parameter * generators: Move variables data to TestMethodData * generators: Make canonical data class immutable * generators: Extract rendering logic from test data to test method body * generators: Replace Exercise and Property with TestedClass and TestedMethod * generators: Update package versions * generators: Don't use sealed * generators: Use UpdateTestClass/UpdateTestMethod/UpdateTestMethodBody to customize output * exercises: Sort namespaces * generator: Make GeneratorExercise API more consistent * generators: Remove unneeded initialization * generators: Rename ExerciseWriter to TestClassFile * generators: Move template specific code to separate namespace * generators: Move helpers to separate namespace * generators: Remove unneeded generator scripts * generators: Update namespace * generators: Add separate test method for empty assertion * generators: Improve naming of test method body implementations * generators: Don't use assert template parameters in generators * generators: Use consistent naming for TestMethodBody parameter * generators: Simplified test method and class code * generators: Make lifecycle of update methods be more sane * generators: Simplify triangle generator * generators: Introduce multiline string type * generators: Remove dynamic extensions * generators: Remove type extensions * generators: Simplify description path logic * generators: Move JToken conversion helpers to separate class * generators: Fix invalid ordering of test case when nesting is used * generators: Remove unused conversion cases * generators: Remove convert helper * generators: Introduce assertion rendering helper * generators: Revert to using string as render output * generators: Use assertion helpers * generators: Add throws assertion generic overload * generators: Use standard value formatter in binary search tree generator * generators: Introduce rendering namespace * generators: Add enum rendering helper * generators: Rename Format to Render * generators: Move rendering logic to separate partial classes * generators: Make list rendering generic * generators: Make array rendering generic * generators: Make dictionary rendering generic * generators: Improved friendly name generation * generators: Make multidimensional array rendering generic * generators: Make tuple rendering generic * isbn-verifier: Update tests * generators: Add friendly name support for char and array types * generators: Refactor dictionary variable rendering * maintainers: Update ErikSchierboom info (#599) * maintainers: Update ErikSchierboom info * generators: Refactor dictionary variable rendering * generators: Refactor multi-line string (variable) rendering * generators: Simplify binary search tree generator * generators: Have variable rendering only return single value * generators: Use Environment.Newline where possible * generators: Simplify ocr-numbers generator * generators: Refactor array variable rendering * generators: Remove unused methods * generators: Simplify string/char escaping code * generators: Add collection initialization renderer * generators: Use variable render helper to render variables * generators: Fix simple-cipher generator * generators: Refactor generators * generators: Correctly render null value * generators: Return correct friendly name for Nullable<T> and ValueTuple<T> types * generators: Allow forced formatting of multi-line array * generators: Fix list-ops nested empty list * generators: Add missing namespace to nucleotide-count generator * generators: Fix failing grep example implementation * exercises: Use shorthand notation for ValueTuple * generators: Fix multiline rendering not handling null references * generators: Make TestClassFile static * generators: Merge TestData with TestMethod * generators: Move TestClass and TestMethod rendering output to separate class * generators: Extract different property handling paths to separate methods * generators: Simplify flatten-array generator * generators: Refactor test method output * generators: Update docs * generators: Add missing links to docs and use consistent naming * go-counting: Add space between operators in example implementation * generators: Use consistent formatting of input and expected values for minesweeper * generators: Increase length for single-line array initializers * generators: Remove automatic multi-line rendering for one-dimensional arrays * generators: Update docs
1 parent c64e503 commit e3142cb

File tree

321 files changed

+4813
-5134
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

321 files changed

+4813
-5134
lines changed

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ packages/
1010
obj/
1111
bin/
1212
*.userprefs
13-
.dotnet/
13+
*.DotSettings.user
1414
.dotnet/
1515

1616
.vs/

docs/GENERATORS.md

Lines changed: 187 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
1-
# Test Generators
1+
# Test generators
22

33
Test generators allow tracks to generate tests automatically without having to write them ourselves. Each test generator reads from the exercise's `canonical data`, which defines the name of the test, its inputs, and outputs. You can read more about exercism's approach to test suites [here](https://github.com/exercism/docs/blob/master/language-tracks/exercises/anatomy/test-suites.md).
44

5-
Generating tests automatically removes any sort of user error when creating tests. We want the tests to be accurate with respect to its canonical data. Test generation also makes it much easier to keep tests up to date. As the canonical data changes, the tests will be automatically updated when the generator for that test is run.
5+
Generating tests automatically removes any sort of user error when creating tests. Furthermore, we want the tests to be accurate with respect to its canonical data. Test generation also makes it much easier to keep tests up to date. As the canonical data changes, the tests will be automatically updated when the generator for that test is run.
66

77
An example of a canonical data file can be found [here](https://github.com/exercism/problem-specifications/blob/master/exercises/bob/canonical-data.json)
88

9-
## Common Terms
9+
## Common terms
10+
1011
When looking through the canonical data and the generator code base, we use a lot of common terminology. This list hopefully clarifies what they represent.
1112

1213
- Canonical Data - Represents the entire test suite.
@@ -16,87 +17,232 @@ When looking through the canonical data and the generator code base, we use a lo
1617
- Input - The input for the test case.
1718
- Expected - The expected value when running the test case.
1819

19-
## Adding A Simple Generator
20-
Adding a test generator file is straightforward. Simply add a new file to the generators folder with the name of the exercise (in PascalCase), and extend the `GeneratorExercise` abstract class.
20+
## Adding a simple generator
21+
22+
Adding a test generator is straightforward. Simply add a new file to the `Exercises/Generators` folder with the name of the exercise (in PascalCase), and create a class that extends the `GeneratorExercise` class.
2123

22-
An example of a simple generator would be the Bob exercise. The source is below, but you can freely view it in the repository [here](https://github.com/exercism/csharp/blob/master/generators/Exercises/Bob.cs).
24+
An example of a simple generator would be the Bob exercise. The source is displayed below, but you can freely view it in the repository [here](https://github.com/exercism/csharp/blob/master/generators/Exercises/Bob.cs).
2325

2426
```csharp
25-
namespace Generators.Exercises
27+
namespace Exercism.CSharp.Exercises.Generators
2628
{
2729
public class Bob : GeneratorExercise
2830
{
2931
}
3032
}
3133
```
3234

33-
This is a fully working generator, no other code needs to be written. However, it's simplicity stems from the fact that the test suite and the program itself are relatively trivial.
35+
This is a fully working generator, no other code needs to be written! However, it's simplicity stems from the fact that the test suite and the program itself are relatively trivial.
36+
37+
## Adding a complex generator
3438

35-
## Adding A Complex Generator
39+
When the generator's default output is not sufficient, you can override the `GeneratorExercise` class' virtual methods to override the default behavior.
3640

37-
A more *complex* generator would be the ComplexNumbers generator found [here](https://github.com/exercism/csharp/blob/master/generators/Exercises/ComplexNumbers.cs).
41+
### Method 1: UpdateTestMethod(TestMethod testMethod)
3842

39-
The `GeneratorExercise` abstract class currently exposes five methods that are used for overriding the default behavior when generating an exercise.
43+
Update the test method that described the test method being generated. When you are required to customize a test generator, overriding this method is virtually always what you want to do.
4044

41-
### void UpdateCanonicalData(CanonicalData canonicalData)
42-
Update the canonical data for a given test.
45+
There are many things that can be customized, of which we'll list the more common usages.
4346

44-
The most common use for this override is to iterate over each of the canonical data cases.
47+
#### Customize test data
4548

46-
As an example, if you wanted to change the default behavior so that when the `Input` value of a test is a negative number, an exception should be thrown, the code would look like this.
49+
It is not uncommon that a generator has to transform its input data or expected value to a different value/representation.
50+
51+
An example of this is the [bracket-push](https://github.com/exercism/csharp/blob/master/generators/Exercises/Generators/BracketPush.cs) generator, which has a `"value"` input value, which is of type `string`. However, this `string` value contains a backslash, which needs to escaped in order for it to be rendered correctly:
4752

4853
```csharp
49-
protected override void UpdateCanonicalData(CanonicalData canonicalData)
54+
protected override void UpdateTestMethod(TestMethod testMethod)
5055
{
51-
foreach (var canonicalDataCase in canonicalData.Cases)
52-
{
53-
var caseInputLessThanZero = (long)canonicalDataCase.Input["number"] < 0;
54-
canonicalDataCase.ExceptionThrown = caseInputLessThanZero ? typeof(ArgumentException) : null;
55-
}
56+
testMethod.Input["value"] = testMethod.Input["value"].Replace("\\", "\\\\");
57+
// [...]
5658
}
5759
```
5860

59-
### HashSet\<string\> AddAdditionalNamespaces()
60-
Allows more namespaces to be added to the test suite.
61+
Another common use case is to handle empty arrays. If an array is empty, its type will default to `JArray`, which doesn't have any type information. To allow the generator to output a correctly typed array, we have to convert the `JArray` to an array first.
6162

62-
The tests use `Xunit` so all tests will automatically include the `Xunit` namespace. However, more advanced tests may require additional namespaces.
63+
An example of this is the [proverb](https://github.com/exercism/csharp/blob/master/generators/Exercises/Generators/Proverb.cs) generator, which converts the `JArray` to an empty `string` array:
6364

6465
```csharp
65-
protected override HashSet<string> AddAdditionalNamespaces()
66+
protected override void UpdateTestMethod(TestMethod testMethod)
6667
{
67-
return new HashSet<string>()
68-
{
69-
typeof(Dictionary<char, int>).Namespace
70-
};
68+
// [...]
69+
70+
if (testMethod.Input["strings"] is JArray)
71+
testMethod.Input["strings"] = Array.Empty<string>();
72+
73+
if (testMethod.Expected is JArray)
74+
testMethod.Expected = Array.Empty<string>();
75+
}
76+
```
77+
78+
#### Output test data as variables
79+
80+
Sometimes, it might make sense to not define a test method's data inline, but as variables.
81+
82+
An example of this is the [crypto-square](https://github.com/exercism/csharp/blob/master/generators/Exercises/Generators/CryptoSquare.cs) generator, which indicates that both the test method input as well as the expected value, should be stored in variables:
83+
84+
```csharp
85+
protected override void UpdateTestMethod(TestMethod testMethod)
86+
{
87+
testMethod.UseVariablesForInput = true;
88+
testMethod.UseVariableForExpected = true;
89+
}
90+
```
91+
92+
#### Custom tested method type
93+
94+
By default, the generator will test a static method. However, you can also test for instance methods, extension methods, properties and constructors.
95+
96+
An example of this is the [roman-numerals](https://github.com/exercism/csharp/blob/master/generators/Exercises/Generators/RomanNumerals.cs) generator, which indicates that it tests an extensions method:
97+
98+
```csharp
99+
protected override void UpdateTestMethod(TestMethod testMethod)
100+
{
101+
testMethod.TestedMethodType = TestedMethodType.ExtensionMethod;
102+
testMethod.TestedMethod = "ToRoman";
71103
}
72104
```
73105

74-
This snippet would add the namespace that the `Dictionary<char, int>` collection lives in (`System.Collections.Generic`).
106+
#### Change names used
107+
108+
As we saw in the previous example, you can also customize the name of the tested method. You are also allowed to customize the tested class' name and the test method name.
75109

76-
### string RenderTestMethodBody[Arrange/Act/Assert]
77-
Override the default behavior when rendering a test methods arrange, act, and/or assert sections.
110+
An example of this is the [triangle](https://github.com/exercism/csharp/blob/master/generators/Exercises/Generators/Triangle.cs) generator, which by default generates duplicate test method names (which will be a compile-time error), but instead uses the `TestMethodNameWithPath` to use the full path as the test method name (effectively making the test method name unique):
111+
112+
```csharp
113+
protected override void UpdateTestMethod(TestMethod testMethod)
114+
{
115+
// [...]
116+
testMethod.TestMethodName = testMethod.TestMethodNameWithPath;
117+
// [...]
118+
}
119+
```
78120

79-
More advanced tests may need to leverage a `template`. A template allows you to add additional code to a test and assert more complex statements.
121+
#### Test for an exception being thrown
80122

81-
An example of this is the [RunLengthEncoding](https://github.com/exercism/csharp/blob/master/generators/Exercises/RunLengthEncoding.cs) test.
123+
Some test methods want to verify that an exception is being thrown.
82124

83-
Here the **Assert** is being overridden. The assert needs to call additional functions, but only if the property is `consistency`. Otherwise, render the assert as usual.
125+
An example of this is the [rna-transcription](https://github.com/exercism/csharp/blob/master/generators/Exercises/Generators/RnaTranscription.cs) generator, which defines that some of its test methods should throw an `ArgumentException`:
84126

85-
### string[] RenderAdditionalMethods()
86-
Allow additional methods to be added to the test suite.
127+
```csharp
128+
protected override void UpdateTestMethod(TestMethod testMethod)
129+
{
130+
if (testMethod.Expected is null)
131+
testMethod.ExceptionThrown = typeof(ArgumentException);
132+
}
133+
```
87134

88-
There may exist cases where a suite of unit tests will need to reuse the same logic in each of the tests. Rather than duplicating code, this method allows you to provide helper methods for the tests.
135+
Note that `ArgumentException` type's namespace will be automatically added to the list of namespaces used in the test class.
89136

90-
An example of this is the [Tournament](https://github.com/exercism/csharp/blob/master/generators/Exercises/Tournament.cs#L45) generator.
137+
#### Custom input/constructor parameters
91138

92-
Additional methods added using this override will be added to the bottom of the test suite.
139+
In some cases, you might want to override the parameters that are used as input parameters.
140+
141+
An example of this is the [two-fer](https://github.com/exercism/csharp/blob/master/generators/Exercises/Generators/TwoFer.cs) generator, which does not use any input parameters when the `"name"` input parameter is set to `null`:
142+
143+
```csharp
144+
protected override void UpdateTestMethod(TestMethod testMethod)
145+
{
146+
// [...]
147+
148+
if (testMethod.Input["name"] is null)
149+
testMethod.InputParameters = Array.Empty<string>();
150+
}
151+
```
152+
153+
If a test method tests an instance method, you can also specify which parameters to use as constructor parameters (the others will be input parameters, unless specified otherwise).
154+
155+
An example of this is the [matrix](https://github.com/exercism/csharp/blob/master/generators/Exercises/Generators/Matrix.cs) generator, which specifies that the `"string"` parameter should be passed as a constructor parameter:
156+
157+
```csharp
158+
protected override void UpdateTestMethod(TestMethod testMethod)
159+
{
160+
testMethod.TestedMethodType = TestedMethodType.InstanceMethod;
161+
testMethod.ConstructorInputParameters = new[] { "string" };
162+
}
163+
```
164+
165+
#### Custom arrange/act/assert code
166+
167+
Although this should be used as a last resort, some generators might want to skip the default generation completely and control which arrange, act or assert code the test method should contain.
168+
169+
An example of this is the [run-length-encoding](https://github.com/exercism/csharp/blob/master/generators/Exercises/Generators/RunLengthEncoding.cs) generator, which uses a custom assertion for one specific property:
170+
171+
```csharp
172+
protected override void UpdateTestMethod(TestMethod testMethod)
173+
{
174+
// [...]
175+
176+
if (testMethod.Property == "consistency")
177+
testMethod.Assert = RenderConsistencyToAssert(testMethod);
178+
}
179+
180+
private string RenderConsistencyToAssert(TestMethod testMethod)
181+
{
182+
var expected = Render.Object(testMethod.Expected);
183+
var actual = $"{testMethod.TestedClass}.Decode({testMethod.TestedClass}.Encode({expected}))";
184+
return Render.AssertEqual(expected, actual);
185+
}
186+
```
187+
188+
Note that the `Render` instance is used to render the assertion and the expected value.
189+
190+
### Method 2: UpdateNamespaces(ISet<string> namespaces)
191+
192+
Allows additional namespaces to be added to the test suite.
193+
194+
All tests use the `Xunit` framework, so each test class will automatically include the `Xunit` namespace. However, some test classes may require additional namespaces.
195+
196+
An example of this is the [gigasecond](https://github.com/exercism/csharp/blob/master/generators/Exercises/Generators/Gigasecond.cs) generator, which uses the `DateTime` class in its test methods, and thus adds its namespace to the list of namespaces:
197+
198+
```csharp
199+
protected override void UpdateNamespaces(ISet<string> namespaces)
200+
{
201+
namespaces.Add(typeof(DateTime).Namespace);
202+
}
203+
```
204+
205+
Note that as mentioned before, the namespace of any thrown exception types are automatically added to the list of namespaces.
206+
207+
### Method 3: UpdateTestClass(TestClass testClass)
208+
209+
This method allows you to customize the output of the test class. Only in rare cases would you want to override this method. The most common use case to override this method, is to add additional (helper) methods to the test suite.
210+
211+
An example of this is the [tournament](https://github.com/exercism/csharp/blob/master/generators/Exercises/Generators/Tournament.cs) generator, which adds a helper method to the test suite:
212+
213+
```csharp
214+
protected override void UpdateTestClass(TestClass testClass)
215+
{
216+
AddRunTallyMethod(testClass);
217+
}
218+
219+
private static void AddRunTallyMethod(TestClass testClass)
220+
{
221+
testClass.AdditionalMethods.Add(@"
222+
private string RunTally(string input)
223+
{
224+
var encoding = new UTF8Encoding();
225+
226+
using (var inStream = new MemoryStream(encoding.GetBytes(input)))
227+
using (var outStream = new MemoryStream())
228+
{
229+
Tournament.Tally(inStream, outStream);
230+
return encoding.GetString(outStream.ToArray());
231+
}
232+
}");
233+
}
234+
```
235+
236+
Additional methods will be added to the bottom of the test suite.
93237

94238
## Updating Existing Files
239+
95240
It is possible that an existing exercise does not match the canonical data. It is OK to update the exercise stub and/or the exercise example to follow the canonical data! An example might be that an exercise is named SumOfMultiples, but the SumOfMultiples.cs and Example.cs files both use `Multiples` as the name of the class.
96241

97242
Also, if you find an issue with one of the existing generators or test suites simply open up the generator that you would like to update, make your changes, and then run the generators.
98243

99244
## Running The Generators
245+
100246
This repository is coded against [.NET Core](https://www.microsoft.com/net/core). To run the generators all you need to do is run the following command in the generators directory:
101247

102248
`dotnet run`
@@ -112,4 +258,5 @@ Once the generator has been run, you can view the output of your generation by n
112258
`exercises/bob/BobTest.cs`
113259

114260
## Submitting A Generator
261+
115262
If you are satisfied with the output of your generator, we would love for you to submit a pull request! Please include your generator, updated test suite, and any other corresponding files that you may have changed.

exercises/all-your-base/AllYourBaseTest.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// This file was auto-generated based on version 2.3.0 of the canonical data.
22

3-
using Xunit;
43
using System;
4+
using Xunit;
55

66
public class AllYourBaseTest
77
{
@@ -89,7 +89,7 @@ public void Number_15_bit_integer()
8989
public void Empty_list()
9090
{
9191
var inputBase = 2;
92-
var digits = new int[0];
92+
var digits = Array.Empty<int>();
9393
var outputBase = 10;
9494
var expected = new[] { 0 };
9595
Assert.Equal(expected, AllYourBase.Rebase(inputBase, digits, outputBase));
@@ -138,7 +138,7 @@ public void Input_base_is_one()
138138
public void Input_base_is_zero()
139139
{
140140
var inputBase = 0;
141-
var digits = new int[0];
141+
var digits = Array.Empty<int>();
142142
var outputBase = 10;
143143
Assert.Throws<ArgumentException>(() => AllYourBase.Rebase(inputBase, digits, outputBase));
144144
}

exercises/alphametics/AlphameticsTest.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
// This file was auto-generated based on version 1.2.0 of the canonical data.
22

3-
using Xunit;
43
using System;
54
using System.Collections.Generic;
5+
using Xunit;
66

77
public class AlphameticsTest
88
{

exercises/binary-search-tree/BinarySearchTreeTest.cs

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// This file was auto-generated based on version 1.0.0 of the canonical data.
22

3-
using Xunit;
43
using System.Linq;
4+
using Xunit;
55

66
public class BinarySearchTreeTest
77
{
@@ -12,19 +12,6 @@ public void Data_is_retained()
1212
Assert.Equal(4, tree.Value);
1313
}
1414

15-
[Fact(Skip = "Remove to run test")]
16-
public void Can_create_complex_tree()
17-
{
18-
var tree = new BinarySearchTree(new[] { 4, 2, 6, 1, 3, 5, 7 });
19-
Assert.Equal(4, tree.Value);
20-
Assert.Equal(2, tree.Left.Value);
21-
Assert.Equal(1, tree.Left.Left.Value);
22-
Assert.Equal(3, tree.Left.Right.Value);
23-
Assert.Equal(6, tree.Right.Value);
24-
Assert.Equal(5, tree.Right.Left.Value);
25-
Assert.Equal(7, tree.Right.Right.Value);
26-
}
27-
2815
[Fact(Skip = "Remove to run test")]
2916
public void Smaller_number_at_left_node()
3017
{
@@ -49,6 +36,19 @@ public void Greater_number_at_right_node()
4936
Assert.Equal(5, tree.Right.Value);
5037
}
5138

39+
[Fact(Skip = "Remove to run test")]
40+
public void Can_create_complex_tree()
41+
{
42+
var tree = new BinarySearchTree(new[] { 4, 2, 6, 1, 3, 5, 7 });
43+
Assert.Equal(4, tree.Value);
44+
Assert.Equal(2, tree.Left.Value);
45+
Assert.Equal(1, tree.Left.Left.Value);
46+
Assert.Equal(3, tree.Left.Right.Value);
47+
Assert.Equal(6, tree.Right.Value);
48+
Assert.Equal(5, tree.Right.Left.Value);
49+
Assert.Equal(7, tree.Right.Right.Value);
50+
}
51+
5252
[Fact(Skip = "Remove to run test")]
5353
public void Can_sort_single_number()
5454
{

0 commit comments

Comments
 (0)