Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion config.json
Original file line number Diff line number Diff line change
Expand Up @@ -857,7 +857,8 @@
],
"ignored": [
"docs",
"img"
"img",
"generators"
],
"foregone": [
"lens-person",
Expand Down
19 changes: 19 additions & 0 deletions generators/CanonicalData.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
using System.ComponentModel.DataAnnotations;
using Newtonsoft.Json;

namespace Generators
{
public class CanonicalData
{
[Required]
public string Exercise { get; set; }

[Required]
public string Version { get; set; }

public string[] Comments { get; set; }

[JsonConverter(typeof(CanonicalDataCasesJsonConverter))]
public CanonicalDataCase[] Cases { get; set; }
}
}
24 changes: 24 additions & 0 deletions generators/CanonicalDataCase.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
using Newtonsoft.Json;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;

namespace Generators
{
[JsonConverter(typeof(CanonicalDataCaseJsonConverter))]
public class CanonicalDataCase
{
[Required]
public string Description { get; set; }

[Required]
public string Property { get; set; }

public string[] Comments { get; set; }

public object Input { get; set; }

public object Expected { get; set; }

public IDictionary<string, object> Data { get; set; }
}
}
32 changes: 32 additions & 0 deletions generators/CanonicalDataCaseJsonConverter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
using System;
using System.Collections.Generic;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;

namespace Generators
{
public class CanonicalDataCaseJsonConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return typeof(CanonicalDataCase) == objectType;
}

public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
var jToken = JToken.ReadFrom(reader);

var canonicalDataCase = new CanonicalDataCase();
serializer.Populate(new JTokenReader(jToken), canonicalDataCase);

canonicalDataCase.Data = jToken.ToObject<IDictionary<string, object>>();

return canonicalDataCase;
}

public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
throw new NotImplementedException();
}
}
}
30 changes: 30 additions & 0 deletions generators/CanonicalDataCasesJsonConverter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
using System;
using System.Collections.Generic;
using System.Reflection;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System.Linq;

namespace Generators
{
public class CanonicalDataCasesJsonConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return typeof(IEnumerable<CanonicalData>).GetTypeInfo().IsAssignableFrom(objectType);
}

public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
var casesToken = JToken.ReadFrom(reader);
var caseTokens = casesToken.SelectTokens("$..*[?(@.property)]").ToArray();

return new JArray(caseTokens).ToObject(objectType);
}

public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
throw new NotImplementedException();
}
}
}
28 changes: 28 additions & 0 deletions generators/CanonicalDataParser.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
using Newtonsoft.Json;
using System;
using System.ComponentModel.DataAnnotations;
using System.Net.Http;

namespace Generators
{
public static class CanonicalDataParser
{
private static readonly HttpClient httpClient = new HttpClient();

public static CanonicalData Parse(string exercise)
{
var canonicalDataJson = DownloadCanonicalDataJson(exercise);
var canonicalData = JsonConvert.DeserializeObject<CanonicalData>(canonicalDataJson);

Validator.ValidateObject(canonicalData, new ValidationContext(canonicalData));

return canonicalData;
}

private static string DownloadCanonicalDataJson(string exercise)
=> httpClient.GetStringAsync(GetCanonicalDataUrl(exercise)).GetAwaiter().GetResult();

private static Uri GetCanonicalDataUrl(string exercise)
=> new Uri($"https://raw.githubusercontent.com/exercism/x-common/master/exercises/{exercise}/canonical-data.json");
}
}
23 changes: 23 additions & 0 deletions generators/ExerciseCollection.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
using Generators.Exercises;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;

namespace Generators
{
public class ExerciseCollection : IEnumerable<Exercise>
{
private readonly IEnumerable<Exercise> generators = GetDefinedGenerators();

public IEnumerator<Exercise> GetEnumerator() => generators.GetEnumerator();

IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();

private static IEnumerable<Exercise> GetDefinedGenerators() =>
from type in Assembly.GetEntryAssembly().GetTypes()
where typeof(Exercise).IsAssignableFrom(type) && !type.GetTypeInfo().IsAbstract
select (Exercise)Activator.CreateInstance(type);
}
}
14 changes: 14 additions & 0 deletions generators/Exercises/Exercise.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
namespace Generators.Exercises
{
public abstract class Exercise
{
protected Exercise(string name)
{
Name = name;
}

public string Name { get; }

public abstract TestClass CreateTestClass(CanonicalData canonicalData);
}
}
12 changes: 12 additions & 0 deletions generators/Generators.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp1.1</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Humanizer" Version="2.1.0" />
<PackageReference Include="Newtonsoft.Json" Version="9.0.1" />
<PackageReference Include="serilog" Version="2.4.0" />
<PackageReference Include="serilog.sinks.literate" Version="2.1.0" />
</ItemGroup>
</Project>
6 changes: 6 additions & 0 deletions generators/Generators.csproj.user
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<ShowAllFiles>false</ShowAllFiles>
</PropertyGroup>
</Project>
22 changes: 22 additions & 0 deletions generators/Generators.sln
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 15
VisualStudioVersion = 15.0.26228.4
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Generators", "Generators.csproj", "{F310316B-5E18-4E7F-A77D-D26AD8D92307}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{F310316B-5E18-4E7F-A77D-D26AD8D92307}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F310316B-5E18-4E7F-A77D-D26AD8D92307}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F310316B-5E18-4E7F-A77D-D26AD8D92307}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F310316B-5E18-4E7F-A77D-D26AD8D92307}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
EndGlobal
59 changes: 59 additions & 0 deletions generators/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
using Generators.Exercises;
using System.IO;
using Humanizer;
using Serilog;

namespace Generators
{
public static class Program
{
public static void Main()
{
SetupLogger();
GenerateAll();
}

private static void SetupLogger()
{
Log.Logger = new LoggerConfiguration()
.WriteTo.LiterateConsole()
.CreateLogger();
}

private static void GenerateAll()
{
Log.Information("Start generating tests...");

foreach (var exercise in new ExerciseCollection())
TestFileGenerator.Generate(exercise);

Log.Information("Finished generating tests for all supported exercises.");
}
}

public static class TestFileGenerator
{
public static void Generate(Exercise exercise)
{
var testClassContents = GenerateTestClassContents(exercise);
var testClassFilePath = TestFilePath(exercise);

SaveTestClassContentsToFile(testClassFilePath, testClassContents);
Log.Information("Generated tests for {Exercise} exercise in {TestFile}.", exercise.Name, testClassFilePath);
}

private static string GenerateTestClassContents(Exercise exercise)
{
var canonicalData = CanonicalDataParser.Parse(exercise.Name);
var testClass = exercise.CreateTestClass(canonicalData);
return TestClassRenderer.Render(testClass);
}

private static void SaveTestClassContentsToFile(string testClassFilePath, string testClassContents) =>
File.WriteAllText(testClassFilePath, testClassContents);

private static string TestFilePath(Exercise exercise) => Path.Combine("..", "exercises", exercise.Name, TestFileName(exercise));

private static string TestFileName(Exercise exercise) => $"{exercise.Name.Transform(Humanizer.To.TitleCase)}Test.cs";
}
}
13 changes: 13 additions & 0 deletions generators/TestClass.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using System.Collections.Generic;

namespace Generators
{
public class TestClass
{
public ISet<string> UsingNamespaces { get; set; } = new HashSet<string> { "Xunit" };
public string ClassName { get; set; }
public string BeforeTestMethods { get; set; }
public TestMethod[] TestMethods { get; set; }
public string AfterTestMethods { get; set; }
}
}
36 changes: 36 additions & 0 deletions generators/TestClassRenderer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
using System.Collections.Generic;
using System.Linq;

namespace Generators
{
public static class TestClassRenderer
{
private const string TestClassTemplate =
@"{UsingNamespaces}

public class {ClassName}Test
{
{Body}
}";

public static string Render(TestClass testClass) =>
TestClassTemplate
.Replace("{UsingNamespaces}", RenderUsingNamespaces(testClass))
.Replace("{ClassName}", testClass.ClassName)
.Replace("{Body}", RenderBody(testClass));

private static string RenderUsingNamespaces(TestClass testClass) =>
string.Join("\n", testClass.UsingNamespaces.Select(usingNamespace => $"using {usingNamespace};"));

private static string RenderBody(TestClass testClass) =>
string.Join("\n\n", GetBodyParts(testClass));

private static IEnumerable<string> GetBodyParts(TestClass testClass) =>
from bodyPart in new [] { testClass.BeforeTestMethods, RenderTestMethods(testClass), testClass.AfterTestMethods }
where !string.IsNullOrWhiteSpace(bodyPart)
select bodyPart;

private static string RenderTestMethods(TestClass testClass) =>
string.Join("\n\n", testClass.TestMethods.Select(TestMethodRenderer.Render));
}
}
9 changes: 9 additions & 0 deletions generators/TestMethod.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace Generators
{
public class TestMethod
{
public int Index { get; set; }
public string MethodName { get; set; }
public string Body { get; set; }
}
}
11 changes: 11 additions & 0 deletions generators/TestMethodNameTransformer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using System.Text.RegularExpressions;
using Humanizer;

namespace Generators
{
public class TestMethodNameTransformer : IStringTransformer
{
public string Transform(string input)
=> Regex.Replace(input, @"[^\w]+", "_", RegexOptions.Compiled).Underscore().Transform(Humanizer.To.TitleCase);
}
}
18 changes: 18 additions & 0 deletions generators/TestMethodRenderer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
namespace Generators
{
public static class TestMethodRenderer
{
private const string TestMethodTemplate =
@" [Fact{Skip}]
public void {Name}()
{
{Body}
}";

public static string Render(TestMethod testMethod) =>
TestMethodTemplate
.Replace("{Name}", testMethod.MethodName)
.Replace("{Body}", testMethod.Body)
.Replace("{Skip}", testMethod.Index == 0 ? "" : "(Skip = \"Remove to run test\")");
}
}
Loading