-
Notifications
You must be signed in to change notification settings - Fork 55
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Auto generate tests #80
Closed
Closed
Changes from 12 commits
Commits
Show all changes
22 commits
Select commit
Hold shift + click to select a range
9a50b1d
Initial commit of auto-generation code. This auto-generates C# code …
thargy eb08e25
Added ToMemorySize() to TestUtils for nicer test output.
thargy dc18c65
Now creates test data, and a result set based on executing on the CPU.
thargy cd4acd0
Added TestTimer to make it easier to output timing message during lon…
thargy 6b2c6df
Runs test on all backends and does naive byte wise comparison of resu…
thargy fa4471e
Added some more utilities methods to improve test output.
thargy 66861b3
Move code into AutoGenerated folder, and split out classes to improve…
thargy a5f54a5
Renamed Mappings.Methods to Mappings.MethodMaps and changed Methods t…
thargy f1000ac
Major refactor to make more OO and easier to understand.
thargy 64f819a
Added more flexible equality that is based on float comparison where …
thargy 0ccdec0
Added float generation control, and float comparison control, to prov…
thargy 14fef61
Merge branch 'master' into AutoGenerateTests
thargy f9a7b57
Added new 'AddCheck' methods to known functions implementations. Thi…
thargy 22bb0bf
A failure to compile a backend will not cause the test set to be skip…
thargy ccf1aa0
Fixed issues with Open GL ES floats, and support for MathF.Pow overl…
thargy d730c31
Fixed issue with Open GL ES float cast bracket position.
thargy 80230d8
Correct to only pass method name into execution code, as per GH-80 co…
thargy 564a26e
Removed resources file as per GH-80 request. Also removed DoCS metho…
thargy 4f40651
Fixed atanh implementaion in HLSL.
thargy adc12a5
Fixed bug with extracting thread ID in compute shader.
thargy 834cd02
Ensure waithandles are disposed after process by changing using order.
thargy fecd9e7
Wrapped waithandle set's in try..catch to prevent occasional race con…
thargy File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,359 @@ | ||
using System; | ||
using System.Collections.Generic; | ||
using System.Linq; | ||
using System.Reflection; | ||
using System.Runtime.InteropServices; | ||
using System.Text; | ||
using Microsoft.CodeAnalysis; | ||
using Microsoft.CodeAnalysis.CSharp; | ||
using ShaderGen.Tests.Tools; | ||
using TestShaders; | ||
using Veldrid; | ||
using Xunit; | ||
using Xunit.Abstractions; | ||
|
||
namespace ShaderGen.Tests.AutoGenerated | ||
{ | ||
public class BuiltinsTests | ||
{ | ||
#region Test Configuration | ||
/// <summary> | ||
/// The skip reason, set to <see langword="null"/> to enable tests in class. | ||
/// </summary> | ||
private const string SkipReason = null; | ||
|
||
/// <summary> | ||
/// The number of failure examples to output. | ||
/// </summary> | ||
private const int FailureExamples = 5; | ||
|
||
/// <summary> | ||
/// Controls the minimum mantissa when generating a floating point number (how 'small' it can go) | ||
/// </summary> | ||
/// <remarks>To test all valid floats this should be set to -126.</remarks> | ||
private static readonly int MinMantissa = -3; | ||
|
||
/// <summary> | ||
/// Controls the maximum mantissa when generating a floating point number (how 'big' it can go) | ||
/// </summary> | ||
/// <remarks>To test all valid floats this should be set to 128.</remarks> | ||
private static readonly int MaxMantissa = 3; | ||
|
||
/// <summary> | ||
/// The float epsilon is used to indicate how close two floats need to be to be considered approximately equal. | ||
/// </summary> | ||
private float FloatEpsilon = 1f; | ||
|
||
/// <summary> | ||
/// The methods to exclude from <see cref="ShaderBuiltins"/> | ||
/// </summary> | ||
/// <remarks>TODO See #78 to show why this is another reason to split ShaderBuiltins.</remarks> | ||
private static readonly HashSet<string> _gpuOnly = new HashSet<string> | ||
{ | ||
nameof(ShaderBuiltins.Sample), | ||
nameof(ShaderBuiltins.SampleGrad), | ||
nameof(ShaderBuiltins.Load), | ||
nameof(ShaderBuiltins.Store), | ||
nameof(ShaderBuiltins.SampleComparisonLevelZero), | ||
nameof(ShaderBuiltins.Discard), | ||
nameof(ShaderBuiltins.ClipToTextureCoordinates), | ||
nameof(ShaderBuiltins.Ddx), | ||
nameof(ShaderBuiltins.DdxFine), | ||
nameof(ShaderBuiltins.Ddy), | ||
nameof(ShaderBuiltins.DdyFine), | ||
nameof(ShaderBuiltins.InterlockedAdd) | ||
}; | ||
|
||
/// <summary> | ||
/// Gets the methods to test. | ||
/// </summary> | ||
/// <value> | ||
/// The methods to test. | ||
/// </value> | ||
private IEnumerable<MethodInfo> MethodsToTest => typeof(ShaderBuiltins) | ||
.GetMethods(BindingFlags.DeclaredOnly | BindingFlags.Static | BindingFlags.Public) | ||
.Where(m => !_gpuOnly.Contains(m.Name) && !m.IsSpecialName) | ||
.OrderBy(m => m.Name); | ||
|
||
/// <summary> | ||
/// The number of test iterations for each backend. | ||
/// </summary> | ||
private const int TestLoops = 1000; | ||
#endregion | ||
|
||
|
||
/// <summary> | ||
/// The output stream for tests. | ||
/// </summary> | ||
private readonly ITestOutputHelper _output; | ||
|
||
/// <summary> | ||
/// Initializes a new instance of the <see cref="BuiltinsTests"/> class. | ||
/// </summary> | ||
/// <param name="output">The output.</param> | ||
public BuiltinsTests(ITestOutputHelper output) | ||
{ | ||
_output = output; | ||
} | ||
|
||
[SkippableFact(typeof(RequiredToolFeatureMissingException), Skip = SkipReason)] | ||
private void TestBuiltins() | ||
{ | ||
// Find all backends that can create a headless graphics device on this system. | ||
IReadOnlyList<ToolChain> toolChains = ToolChain.Requires(ToolFeatures.HeadlessGraphicsDevice, false); | ||
if (toolChains.Count < 1) | ||
{ | ||
throw new RequiredToolFeatureMissingException( | ||
$"At least one tool chain capable of creating headless graphics devices is required for this test!"); | ||
} | ||
|
||
string csFunctionName = "ComputeShader.CS"; | ||
|
||
/* | ||
* Auto-generate C# code for testing methods. | ||
*/ | ||
IReadOnlyList<MethodInfo> methods = null; | ||
Mappings mappings; | ||
Compilation compilation; | ||
using (new TestTimer(_output, () => $"Generating C# shader code to test {methods.Count} methods")) | ||
{ | ||
// Get all the methods we wish to test | ||
methods = MethodsToTest.ToArray(); | ||
mappings = CreateMethodTestCompilation(methods, out compilation); | ||
|
||
// Note, you could use compilation.Emit(...) at this point to compile the auto-generated code! | ||
// however, for now we'll invoke methods directly rather than executing the C# code that has been | ||
// generated, as loading emitted code into a test is currently much more difficult. | ||
} | ||
|
||
// Allocate enough space to store the result sets for each backend! | ||
TestSets testSets = null; | ||
using (new TestTimer( | ||
_output, | ||
() => $"Generating random test data ({(mappings.BufferSize * testSets.TestLoops).ToMemorySize()}) for {testSets.TestLoops} iterations of {mappings.Methods} methods") | ||
) | ||
{ | ||
testSets = new TestSets(toolChains, compilation, mappings, TestLoops, MinMantissa, MaxMantissa); | ||
} | ||
|
||
/* | ||
* Transpile shaders | ||
*/ | ||
|
||
ShaderGenerationResult generationResult; | ||
|
||
using (new TestTimer( | ||
_output, | ||
t => | ||
$"Generated shader sets for {string.Join(", ", toolChains.Select(tc => tc.Name))} backends in {t * 1000:#.##}ms.") | ||
) | ||
{ | ||
ShaderGenerator sg = new ShaderGenerator( | ||
compilation, | ||
testSets.Select(t => t.Backend).Where(b => b != null).ToArray(), | ||
null, | ||
null, | ||
csFunctionName); | ||
|
||
generationResult = sg.GenerateShaders(); | ||
} | ||
|
||
/* | ||
* Loop through each backend to run tests. | ||
*/ | ||
bool first = true; | ||
using (new TestTimer(_output, "Executing all tests on all backends")) | ||
{ | ||
foreach (TestSet testSet in testSets) | ||
{ | ||
_output.WriteLine(string.Empty); | ||
if (first) | ||
{ | ||
// This is the first test set, so we use Space1 instead of Spacer 2. | ||
first = false; | ||
_output.WriteLine(TestUtil.Spacer1); | ||
} | ||
else | ||
{ | ||
_output.WriteLine(TestUtil.Spacer2); | ||
} | ||
_output.WriteLine(String.Empty); | ||
|
||
testSet.Execute(generationResult, csFunctionName, _output); | ||
} | ||
|
||
_output.WriteLine(string.Empty); | ||
} | ||
|
||
_output.WriteLine(string.Empty); | ||
_output.WriteLine(TestUtil.Spacer1); | ||
_output.WriteLine(string.Empty); | ||
|
||
/* | ||
* Finally, evaluate differences between results | ||
*/ | ||
IReadOnlyList<(MethodMap MethodMap, IReadOnlyList<Failure> Failures)> failures; | ||
using (new TestTimer(_output, "Analysing results for failures")) | ||
{ | ||
failures = testSets.GetFailures(FloatEpsilon) | ||
.Select(kvp => (MethodMap: kvp.Key, Failures: kvp.Value)) | ||
.OrderByDescending(kvp => kvp.Failures.Count) | ||
.ToArray(); | ||
} | ||
|
||
if (!failures.Any()) | ||
{ | ||
_output.WriteLine("No failures detected!"); | ||
return; | ||
} | ||
|
||
_output.WriteLine($"{failures.Count} methods had failures out of {mappings.Methods} ({100.0 * failures.Count / mappings.Methods:#.##}%)."); | ||
|
||
_output.WriteLine(string.Empty); | ||
|
||
// Get pointer array | ||
string lastMethodName = null; | ||
foreach ((MethodMap methodMap, IReadOnlyList<Failure> methodFailures) in failures) | ||
{ | ||
if (lastMethodName != methodMap.Method.Name) | ||
{ | ||
if (lastMethodName != null) | ||
{ | ||
// Seperate methods of different names with spacer 1 | ||
_output.WriteLine(string.Empty); | ||
_output.WriteLine(TestUtil.Spacer1); | ||
_output.WriteLine(string.Empty); | ||
} | ||
|
||
lastMethodName = methodMap.Method.Name; | ||
} | ||
else | ||
{ | ||
_output.WriteLine(string.Empty); | ||
_output.WriteLine(TestUtil.Spacer2); | ||
_output.WriteLine(string.Empty); | ||
} | ||
|
||
int failureCount = methodFailures.Count; | ||
_output.WriteLine( | ||
$"{TestUtil.GetUnicodePieChart((double)failureCount / testSets.TestLoops)} {methodMap.Signature} failed {failureCount}/{testSets.TestLoops} ({failureCount * 100.0 / testSets.TestLoops:#.##}%)."); | ||
|
||
// Output examples! | ||
int example = 0; | ||
foreach (Failure failure in methodFailures) | ||
{ | ||
_output.WriteLine(string.Empty); | ||
if (example++ >= FailureExamples) | ||
{ | ||
_output.WriteLine("…"); | ||
break; | ||
} | ||
|
||
_output.WriteLine(failure.ToString()); | ||
} | ||
} | ||
|
||
_output.WriteLine(string.Empty); | ||
_output.WriteLine(TestUtil.Spacer2); | ||
_output.WriteLine(string.Empty); | ||
} | ||
|
||
/// <summary> | ||
/// Creates the method test compilation. | ||
/// </summary> | ||
/// <param name="methods">The methods.</param> | ||
/// <param name="compilation">The compilation.</param> | ||
/// <returns></returns> | ||
private Mappings CreateMethodTestCompilation(IReadOnlyCollection<MethodInfo> methods, out Compilation compilation) | ||
{ | ||
Assert.NotNull(methods); | ||
Assert.NotEmpty(methods); | ||
|
||
// Create compilation | ||
CSharpCompilationOptions cSharpCompilationOptions = | ||
new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary, allowUnsafe: true); | ||
compilation = CSharpCompilation.Create( | ||
"TestAssembly", | ||
null, | ||
TestUtil.ProjectReferences, | ||
cSharpCompilationOptions); | ||
|
||
// Temporary structure to hold method maps until after we calculate input structure. | ||
var methodMaps = | ||
new(int Index, MethodInfo Method, IReadOnlyDictionary<ParameterInfo, string> Parameters, string ReturnField)[methods.Count]; | ||
|
||
PaddedStructCreator paddedStructCreator = new PaddedStructCreator(compilation); | ||
|
||
StringBuilder codeBuilder = new StringBuilder(); | ||
codeBuilder.Append(Resource.SBSP1); | ||
codeBuilder.Append(methods.Count); | ||
codeBuilder.Append(Resource.SBSP2); | ||
|
||
StringBuilder argsBuilder = new StringBuilder(); | ||
/* | ||
* Output test cases | ||
*/ | ||
int methodNumber = 0; | ||
foreach (MethodInfo method in methods) | ||
{ | ||
Assert.True(method.IsStatic); | ||
|
||
ParameterInfo[] parameterInfos = method.GetParameters(); | ||
Dictionary<ParameterInfo, string> parameterMap = | ||
new Dictionary<ParameterInfo, string>(parameterInfos.Length); | ||
|
||
foreach (ParameterInfo parameterInfo in parameterInfos) | ||
{ | ||
if (argsBuilder.Length > 0) | ||
{ | ||
argsBuilder.Append(","); | ||
} | ||
|
||
string fieldName = paddedStructCreator.GetFieldName(parameterInfo.ParameterType); | ||
parameterMap.Add(parameterInfo, fieldName); | ||
argsBuilder.Append(Resource.SBSParam.Replace("$$NAME$$", fieldName)); | ||
} | ||
|
||
string returnName = method.ReturnType != typeof(void) | ||
? paddedStructCreator.GetFieldName(method.ReturnType) | ||
: null; | ||
|
||
string output = returnName != null | ||
? Resource.SBSParam.Replace("$$NAME$$", returnName) + " = " | ||
: string.Empty; | ||
|
||
codeBuilder.Append(Resource.SBSCase | ||
.Replace("$$CASE$$", methodNumber.ToString()) | ||
.Replace("$$RESULT$$", output) | ||
.Replace("$$METHOD$$", $"{method.DeclaringType.FullName}.{method.Name}") | ||
.Replace("$$ARGS$$", argsBuilder.ToString())); | ||
|
||
methodMaps[methodNumber] = (methodNumber++, method, parameterMap, returnName); | ||
paddedStructCreator.Reset(); | ||
argsBuilder.Clear(); | ||
} | ||
|
||
codeBuilder.Append(Resource.SBSP3); | ||
|
||
/* | ||
* Output test fields | ||
*/ | ||
IReadOnlyList<PaddedStructCreator.Field> fields = paddedStructCreator.GetFields(out int bufferSize); | ||
int size = 0; | ||
foreach (PaddedStructCreator.Field field in fields) | ||
{ | ||
codeBuilder.AppendLine($" // {size,3}: Alignment = {field.AlignmentInfo.ShaderAlignment} {(field.IsPaddingField ? " [PADDING}" : string.Empty)}"); | ||
codeBuilder.AppendLine($" {(field.IsPaddingField ? "private" : "public")} {field.Type.FullName} {field.Name};"); | ||
codeBuilder.AppendLine(string.Empty); | ||
size += field.AlignmentInfo.ShaderSize; | ||
} | ||
Assert.Equal(size, bufferSize); | ||
|
||
codeBuilder.Append(Resource.SBSP4); | ||
|
||
string code = codeBuilder.ToString(); | ||
compilation = compilation.AddSyntaxTrees(CSharpSyntaxTree.ParseText(code)); | ||
return new Mappings(bufferSize, fields.ToDictionary(f => f.Name), methodMaps); | ||
} | ||
} | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The "function name" used here should actually just be the function name alone (not including type), because this is passed through to Veldrid. So it should just be "CS". This causes the Metal tests to fail.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Changing to just "CS" threw:
So this may be a problem.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry, maybe I commented on the wrong place. ShaderGen indeed wants the fully-qualified name, because it needs all that info to locate the function. When you pass the actual shader code to Veldrid, on the other hand, you should just pass the actual function name ("CS"). I thought the line I commented on was the variable that ultimately goes into Veldrid.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Got you, I just spotted that and have fixed in latest push.