-
Notifications
You must be signed in to change notification settings - Fork 16
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #2232 from andy840119/add-arch-net
Add the architecture test for this project.
- Loading branch information
Showing
38 changed files
with
688 additions
and
43 deletions.
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,78 @@ | ||
// Copyright (c) andy840119 <[email protected]>. Licensed under the GPL Licence. | ||
// See the LICENCE file in the repository root for full licence text. | ||
|
||
using System; | ||
using System.Linq; | ||
using ArchUnitNET.Domain; | ||
using ArchUnitNET.Loader; | ||
using NUnit.Framework; | ||
|
||
namespace osu.Game.Rulesets.Karaoke.Architectures; | ||
|
||
public abstract class BaseTest | ||
{ | ||
private Project.ProjectAttribute? executeProject; | ||
|
||
[SetUp] | ||
public void SetUp() | ||
{ | ||
executeProject = null; | ||
} | ||
|
||
#region Utility | ||
|
||
protected Project.ProjectAttribute GetExecuteProject() | ||
{ | ||
return executeProject ??= MethodUtils.GetExecuteProject(); | ||
} | ||
|
||
protected Architecture GetProjectArchitecture(params Project.ProjectAttribute[] extraProjects) | ||
{ | ||
// trying to get the callstack with test attribute | ||
var rootObject = GetExecuteProject().RootObjectType; | ||
var assembly = rootObject.Assembly; | ||
|
||
// note: | ||
// 1. only load the test assembly because loading too much assembly will cause the test to be slow. | ||
// 2. should not filter the namespace in here because it will cause inner class or inherit class cannot be found. | ||
return new ArchLoader() | ||
.LoadAssembly(assembly) | ||
.LoadAssemblies(extraProjects.Select(x => x.RootObjectType.Assembly).ToArray()) | ||
.Build(); | ||
} | ||
|
||
protected void Assertion(Action assert) | ||
{ | ||
// check the execute project type | ||
var executeType = GetExecuteProject().ExecuteType; | ||
|
||
switch (executeType) | ||
{ | ||
case Project.ExecuteType.Check: | ||
{ | ||
assert(); | ||
break; | ||
} | ||
|
||
case Project.ExecuteType.Report: | ||
{ | ||
Assert.Multiple(() => | ||
{ | ||
assert(); | ||
|
||
int totalCount = TestContext.CurrentContext.AssertCount; | ||
int failedCount = TestContext.CurrentContext.Result.Assertions.Count(); | ||
Console.WriteLine("================================="); | ||
Console.WriteLine($"There are {failedCount} failed in {totalCount} test step."); | ||
}); | ||
|
||
break; | ||
} | ||
|
||
default: | ||
throw new ArgumentOutOfRangeException(); | ||
} | ||
} | ||
|
||
#endregion | ||
} |
60 changes: 60 additions & 0 deletions
60
osu.Game.Rulesets.Karaoke.Architectures/Edit/Checks/TestCheck.cs
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,60 @@ | ||
// Copyright (c) andy840119 <[email protected]>. Licensed under the GPL Licence. | ||
// See the LICENCE file in the repository root for full licence text. | ||
|
||
using System.Linq; | ||
using ArchUnitNET.Domain.Extensions; | ||
using NUnit.Framework; | ||
using osu.Game.Rulesets.Edit.Checks.Components; | ||
|
||
namespace osu.Game.Rulesets.Karaoke.Architectures.Edit.Checks; | ||
|
||
public class TestCheck : BaseTest | ||
{ | ||
[Test] | ||
[Project.Karaoke(true)] | ||
public void CheckCheckClassNamingAndInherit() | ||
{ | ||
var architecture = GetProjectArchitecture(); | ||
|
||
var baseIssue = architecture.GetClassOfType(typeof(Issue)); | ||
var issues = architecture.Classes.Where(x => x.Namespace.RelativeNameMatches(GetExecuteProject(), "Edit.Checks.Issues")).ToArray(); | ||
|
||
var checkOrIssueTemplate = architecture.Classes.Where(x => x.Namespace.RelativeNameMatches(GetExecuteProject(), "Edit.Checks")).ToArray(); | ||
|
||
var baseCheck = architecture.GetInterfaceOfType(typeof(ICheck)); | ||
var checks = checkOrIssueTemplate.Where(x => x.IsNested == false).ToArray(); | ||
|
||
var baseIssueTemplate = architecture.GetClassOfType(typeof(IssueTemplate)); | ||
var issueTemplates = checkOrIssueTemplate.Where(x => x.IsNested).ToArray(); | ||
|
||
Assertion(() => | ||
{ | ||
// issues. | ||
Assert.NotZero(issues.Length, $"{nameof(Issue)} amount is weird."); | ||
|
||
foreach (var checkClass in issues) | ||
{ | ||
Assert.True(checkClass.InheritedClasses.Contains(baseIssue), $"Class inherit is invalid: {checkClass}"); | ||
Assert.True(checkClass.NameEndsWith("Issue"), $"Class name is invalid: {checkClass}"); | ||
} | ||
|
||
// checks | ||
Assert.NotZero(checks.Length, $"{nameof(ICheck)} amount is weird."); | ||
|
||
foreach (var check in checks) | ||
{ | ||
Assert.True(check.ImplementsInterface(baseCheck), $"Class inherit is invalid: {check}"); | ||
Assert.True(check.NameStartsWith("Check"), $"Class name is invalid: {check}"); | ||
} | ||
|
||
// issue templates. | ||
Assert.NotZero(issueTemplates.Length, $"{nameof(IssueTemplate)} amount is weird"); | ||
|
||
foreach (var checkClass in issueTemplates) | ||
{ | ||
Assert.True(checkClass.InheritedClasses.Contains(baseIssueTemplate), $"Class inherit is invalid: {checkClass}"); | ||
Assert.True(checkClass.NameStartsWith("IssueTemplate"), $"Class name is invalid: {checkClass}"); | ||
} | ||
}); | ||
} | ||
} |
87 changes: 87 additions & 0 deletions
87
osu.Game.Rulesets.Karaoke.Architectures/Edit/Checks/TestCheckTest.cs
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,87 @@ | ||
// Copyright (c) andy840119 <[email protected]>. Licensed under the GPL Licence. | ||
// See the LICENCE file in the repository root for full licence text. | ||
|
||
using System.Linq; | ||
using ArchUnitNET.Domain; | ||
using ArchUnitNET.Domain.Extensions; | ||
using NUnit.Framework; | ||
using osu.Game.Rulesets.Karaoke.Tests.Editor.Checks; | ||
|
||
namespace osu.Game.Rulesets.Karaoke.Architectures.Edit.Checks; | ||
|
||
public class TestCheckTest : BaseTest | ||
{ | ||
[Test] | ||
[Project.KaraokeTest(true)] | ||
public void CheckShouldContainsTest() | ||
{ | ||
var architecture = GetProjectArchitecture(new Project.KaraokeAttribute()); | ||
|
||
var baseCheckTest = architecture.GetClassOfType(typeof(BaseCheckTest<>)); | ||
var allChecks = architecture.Classes | ||
.Where(x => x.Namespace.RelativeNameMatches(new Project.KaraokeAttribute(), "Edit.Checks")) | ||
.Where(x => x.IsNested == false && x.IsAbstract == false) | ||
.ToArray(); | ||
var allCheckTests = architecture.Classes.Where(x => x.InheritedClasses.Contains(baseCheckTest)).ToArray(); | ||
|
||
Assertion(() => | ||
{ | ||
Assert.NotZero(allChecks.Length, "No check found"); | ||
|
||
foreach (var check in allChecks) | ||
{ | ||
// need to make sure that all checks have a test class. | ||
var matchedTest = allCheckTests.FirstOrDefault(x => x.NameContains(check.Name)); | ||
Assert.IsTrue(matchedTest != null, $"Check {check} should have a test class."); | ||
|
||
// need to make sure that all issue template should be tested. | ||
var innerIssueTemplates = check.GetInnerClasses(); | ||
|
||
foreach (var issueTemplate in innerIssueTemplates) | ||
{ | ||
Assert.IsTrue(check.GetTypeDependencies().Contains(issueTemplate), $"Seems {issueTemplate} is not tested."); | ||
} | ||
} | ||
}); | ||
} | ||
|
||
[Test] | ||
[Project.KaraokeTest(true)] | ||
public void CheckTestMethod() | ||
{ | ||
var architecture = GetProjectArchitecture(); | ||
var baseCheckTest = architecture.GetClassOfType(typeof(BaseCheckTest<>)); | ||
|
||
var assertOkMethod = baseCheckTest.GetMethodMembersContainsName("AssertOk").FirstOrDefault(); | ||
var assertNotOkMethod = baseCheckTest.GetMethodMembersContainsName("AssertNotOk").FirstOrDefault(); | ||
|
||
var allCheckTests = architecture.Classes.Where(x => x.InheritedClasses.Contains(baseCheckTest) && x.IsAbstract == false).ToArray(); | ||
|
||
Assertion(() => | ||
{ | ||
Assert.NotNull(assertOkMethod, "AssertOk method not found"); | ||
Assert.NotNull(assertNotOkMethod, "AssertNotOk method not found"); | ||
|
||
Assert.NotZero(allCheckTests.Length, "No check test found"); | ||
|
||
foreach (var checkTest in allCheckTests) | ||
{ | ||
var testMethods = checkTest.GetAllTestMembers(architecture).ToArray(); | ||
Assert.NotZero(testMethods.Length, $"No test method in the {checkTest}"); | ||
|
||
foreach (var testMethod in testMethods) | ||
{ | ||
Assert.IsTrue(isTestMethod(testMethod), $"Test method {testMethod} should call {assertOkMethod} or {assertNotOkMethod} method."); | ||
} | ||
} | ||
|
||
return; | ||
|
||
static bool isTestMethod(IMember testMethod) | ||
{ | ||
var calledMethods = testMethod.GetCalledMethods().ToArray(); | ||
return calledMethods.Any(x => x.NameStartsWith("AssertOk") || x.NameStartsWith("AssertNotOk")); | ||
} | ||
}); | ||
} | ||
} |
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,96 @@ | ||
// Copyright (c) andy840119 <[email protected]>. Licensed under the GPL Licence. | ||
// See the LICENCE file in the repository root for full licence text. | ||
|
||
using System; | ||
using System.Collections.Generic; | ||
using System.Linq; | ||
using System.Text.RegularExpressions; | ||
using ArchUnitNET.Domain; | ||
using ArchUnitNET.Domain.Extensions; | ||
using NUnit.Framework; | ||
using Attribute = ArchUnitNET.Domain.Attribute; | ||
|
||
namespace osu.Game.Rulesets.Karaoke.Architectures; | ||
|
||
public static class Extensions | ||
{ | ||
#region Class | ||
|
||
public static IEnumerable<Class> GetInnerClasses(this Class @class) | ||
{ | ||
// do the same things as type.GetNestedTypes(); | ||
// see: https://learn.microsoft.com/en-us/dotnet/api/system.type.getnestedtypes?view=net-8.0&redirectedfrom=MSDN#overloads | ||
// note: cannot use @class.GetNestedTypes(); because it will return the nested class in the same file. | ||
return @class.GetTypeDependencies().Where(x => x.IsNested).OfType<Class>().Distinct(); | ||
} | ||
|
||
public static IEnumerable<IMember> GetAllTestMembers(this Class @class, Architecture architecture) | ||
{ | ||
var testAttribute = architecture.GetAttributeOfType(typeof(TestAttribute)); | ||
var testCaseAttribute = architecture.GetAttributeOfType(typeof(TestCaseAttribute)); | ||
return @class.MembersIncludingInherited.Where(x => x.Attributes.Contains(testAttribute) || x.Attributes.Contains(testCaseAttribute)); | ||
} | ||
|
||
public static bool HasAttributeInSelfOrChild(this Class @class, Attribute attribute) | ||
{ | ||
return @class.Attributes.Contains(attribute) || @class.InheritedClasses.Any(c => c.Attributes.Contains(attribute)); | ||
} | ||
|
||
#endregion | ||
|
||
#region Name | ||
|
||
public static bool RelativeNameStartsWith( | ||
this IHasName cls, | ||
Project.ProjectAttribute project, | ||
string pattern, | ||
StringComparison stringComparison = StringComparison.CurrentCulture) | ||
{ | ||
string relativeNamespace = getRelativeNamespace(project, cls.FullName); | ||
return relativeNamespace.StartsWith(pattern, stringComparison); | ||
} | ||
|
||
public static bool RelativeNameMatches(this IHasName cls, | ||
Project.ProjectAttribute project, | ||
string pattern, | ||
bool useRegularExpressions = false) | ||
{ | ||
string relativeNamespace = getRelativeNamespace(project, cls.FullName); | ||
|
||
if (!useRegularExpressions) | ||
return string.Equals(relativeNamespace, pattern, StringComparison.OrdinalIgnoreCase); | ||
|
||
return Regex.IsMatch(relativeNamespace, pattern); | ||
} | ||
|
||
private static string getRelativeNamespace(Project.ProjectAttribute project, string fullNamespace) | ||
{ | ||
string? rootObjectNamespace = project.RootObjectType.Namespace; | ||
if (rootObjectNamespace == null) | ||
throw new NotSupportedException("Root object namespace should not be null."); | ||
|
||
if (!fullNamespace.StartsWith(rootObjectNamespace, StringComparison.Ordinal)) | ||
throw new NotSupportedException("The namespace of the class is not in the root object namespace."); | ||
|
||
// remove the start namespace with dot. | ||
return fullNamespace == rootObjectNamespace | ||
? string.Empty | ||
: fullNamespace[$"{rootObjectNamespace}.".Length..]; | ||
} | ||
|
||
#endregion | ||
|
||
#region Type | ||
|
||
public static IEnumerable<MethodMember> GetMethodMembersContainsName(this IType type, string name) | ||
{ | ||
return type.GetMethodMembers().WhereNameContains<MethodMember>(name); | ||
} | ||
|
||
public static IEnumerable<TType> WhereNameContains<TType>(this IEnumerable<TType> source, string name) where TType : IHasName | ||
{ | ||
return source.Where(hasName => hasName.Name.Contains(name)); | ||
} | ||
|
||
#endregion | ||
} |
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,53 @@ | ||
// Copyright (c) andy840119 <[email protected]>. Licensed under the GPL Licence. | ||
// See the LICENCE file in the repository root for full licence text. | ||
|
||
using System; | ||
using System.Diagnostics; | ||
using System.Linq; | ||
using System.Reflection; | ||
using NUnit.Framework; | ||
|
||
namespace osu.Game.Rulesets.Karaoke.Architectures; | ||
|
||
public class MethodUtils | ||
{ | ||
public static Project.ProjectAttribute GetExecuteProject() | ||
{ | ||
// trying to get the callstack with test attribute | ||
var stackTrace = new StackTrace(); | ||
var frames = stackTrace.GetFrames(); | ||
|
||
foreach (var frame in frames) | ||
{ | ||
var method = frame.GetMethod(); | ||
if (method == null) | ||
continue; | ||
|
||
var attributes = method.CustomAttributes; | ||
if (attributes.All(x => x.AttributeType != typeof(TestAttribute))) | ||
continue; | ||
|
||
return getDefaultAttributeByMethod(method); | ||
} | ||
|
||
throw new InvalidOperationException("Test method is not in the callstack."); | ||
|
||
static Project.ProjectAttribute getDefaultAttributeByMethod(MethodBase method) | ||
{ | ||
var projects = method.CustomAttributes | ||
.Where(x => x.AttributeType.BaseType == typeof(Project.ProjectAttribute)) | ||
.Select(x => | ||
{ | ||
object?[] constructorParams = x.ConstructorArguments.Select(args => args.Value).ToArray(); | ||
object? instance = Activator.CreateInstance(x.AttributeType, constructorParams); | ||
|
||
if (instance is not Project.ProjectAttribute propertyAttribute) | ||
throw new InvalidOperationException("The attribute is not found."); | ||
|
||
return propertyAttribute; | ||
}); | ||
|
||
return projects.Single(x => x.Execute); | ||
} | ||
} | ||
} |
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,14 @@ | ||
// Copyright (c) andy840119 <[email protected]>. Licensed under the GPL Licence. | ||
// See the LICENCE file in the repository root for full licence text. | ||
|
||
using System; | ||
|
||
namespace osu.Game.Rulesets.Karaoke.Architectures; | ||
|
||
public static class Program | ||
{ | ||
public static void Main() | ||
{ | ||
Console.WriteLine("This is the project for checking the architecture is designed as expected!"); | ||
} | ||
} |
Oops, something went wrong.