diff --git a/NahidaProject-UnitTestAdapter/NahidaProject-UnitTestAdapter/NahidaProject_UnitTestAdapterPackage.cs b/NahidaProject-UnitTestAdapter/NahidaProject-UnitTestAdapter/NahidaProject_UnitTestAdapterPackage.cs deleted file mode 100644 index 2ffb1148274c2fb4bd8bb73ca7d49d6e35bb98cf..0000000000000000000000000000000000000000 --- a/NahidaProject-UnitTestAdapter/NahidaProject-UnitTestAdapter/NahidaProject_UnitTestAdapterPackage.cs +++ /dev/null @@ -1,53 +0,0 @@ -using Microsoft.VisualStudio.Shell; -using System; -using System.Runtime.InteropServices; -using System.Threading; -using Task = System.Threading.Tasks.Task; - -namespace NahidaProject_UnitTestAdapter -{ - /// - /// This is the class that implements the package exposed by this assembly. - /// - /// - /// - /// The minimum requirement for a class to be considered a valid package for Visual Studio - /// is to implement the IVsPackage interface and register itself with the shell. - /// This package uses the helper classes defined inside the Managed Package Framework (MPF) - /// to do it: it derives from the Package class that provides the implementation of the - /// IVsPackage interface and uses the registration attributes defined in the framework to - /// register itself and its components with the shell. These attributes tell the pkgdef creation - /// utility what data to put into .pkgdef file. - /// - /// - /// To get loaded into VS, the package must be referred by <Asset Type="Microsoft.VisualStudio.VsPackage" ...> in .vsixmanifest file. - /// - /// - [PackageRegistration(UseManagedResourcesOnly = true, AllowsBackgroundLoading = true)] - [Guid(NahidaProject_UnitTestAdapterPackage.PackageGuidString)] - public sealed class NahidaProject_UnitTestAdapterPackage : AsyncPackage - { - /// - /// NahidaProject_UnitTestAdapterPackage GUID string. - /// - public const string PackageGuidString = "84132a2c-5bdf-48b1-bb36-922aed519d10"; - - #region Package Members - - /// - /// Initialization of the package; this method is called right after the package is sited, so this is the place - /// where you can put all the initialization code that rely on services provided by VisualStudio. - /// - /// A cancellation token to monitor for initialization cancellation, which can occur when VS is shutting down. - /// A provider for progress updates. - /// A task representing the async work of package initialization, or an already completed task if there is none. Do not return null from this method. - protected override async Task InitializeAsync(CancellationToken cancellationToken, IProgress progress) - { - // When initialized asynchronously, the current thread may be a background thread at this point. - // Do any initialization that requires the UI thread after switching to the UI thread. - await this.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken); - } - - #endregion - } -} diff --git a/NahidaProject-UnitTestAdapter/NahidaProject-UnitTestAdapter/NahidaUnitTestAdapter.cs b/NahidaProject-UnitTestAdapter/NahidaProject-UnitTestAdapter/NahidaUnitTestAdapter.cs deleted file mode 100644 index b538674c3048fe475cb3343a598aace0b2181f76..0000000000000000000000000000000000000000 --- a/NahidaProject-UnitTestAdapter/NahidaProject-UnitTestAdapter/NahidaUnitTestAdapter.cs +++ /dev/null @@ -1,104 +0,0 @@ -using Microsoft.VisualStudio.TestPlatform.ObjectModel; -using Microsoft.VisualStudio.TestPlatform.ObjectModel.Adapter; -using Microsoft.VisualStudio.TestPlatform.ObjectModel.Logging; -using System; -using System.Collections.Generic; - -namespace NahidaProject_UnitTestAdapter { - [FileExtension(".exe")] - [DefaultExecutorUri(NahidaUnitTestConstant.ExecutorUri)] - public class NahidaUnitTestAdapter : ITestDiscoverer { - public void DiscoverTests(IEnumerable sources, IDiscoveryContext discoveryContext, IMessageLogger logger, ITestCaseDiscoverySink discoverySink) { - logger.SendMessage(TestMessageLevel.Informational, "Starting C++ test discovery..."); - - foreach (var source in sources) { - try { - DiscoverTestsFromExecutable(source, logger, discoverySink); - } - catch (Exception exception) { - logger.SendMessage(TestMessageLevel.Error, $"Error discovering tests from {source}: {exception.Message}"); - } - } - } - - private void DiscoverTestsFromExecutable(string executablePath, IMessageLogger logger, ITestCaseDiscoverySink discoverySink) { - // 运行测试可执行文件获取测试列表 - var testList = GetTestListFromExecutable(executablePath, logger); - - foreach (var testInfo in testList){ - var testCase = new TestCase(testInfo.FullyQualifiedName, new Uri(NahidaUnitTestConstant.ExecutorUri),executablePath) { - DisplayName = testInfo.DisplayName, - CodeFilePath = testInfo.SourceFile, - LineNumber = testInfo.LineNumber - }; - - // 添加自定义属性 - testCase.Traits.Add(new Trait("Category", "CppTest")); - testCase.Traits.Add(new Trait("Suite", testInfo.TestSuite)); - - discoverySink.SendTestCase(testCase); - } - } - - private List GetTestListFromExecutable(string executablePath, IMessageLogger logger){ - var testList = new List(); - - try{ - // 这里需要根据具体的 C++ 测试框架实现 - // 示例:假设使用 Google Test 并支持 --gtest_list_tests 参数 - var processInfo = new System.Diagnostics.ProcessStartInfo { - FileName = executablePath, - Arguments = "", - RedirectStandardOutput = true, - RedirectStandardError = true, - UseShellExecute = false, - CreateNoWindow = true - }; - - using (var process = System.Diagnostics.Process.Start(processInfo)) { - string output = process.StandardOutput.ReadToEnd(); - process.WaitForExit(); - - testList = ParseGTestListOutput(output, executablePath); - } - } - catch (Exception ex) - { - logger.SendMessage(TestMessageLevel.Warning, - $"Could not get test list from {executablePath}: {ex.Message}"); - } - - return testList; - } - - private List ParseGTestListOutput(string output, string executablePath) { - var testList = new List(); - var lines = output.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries); - - string currentSuite = string.Empty; - - foreach (var line in lines) - { - if (line.StartsWith(" ")) // 测试用例 - { - var testName = line.Trim(); - var testInfo = new NahidaUnitTestData - { - FullyQualifiedName = $"{currentSuite}.{testName}", - DisplayName = testName, - TestSuite = currentSuite, - SourceFile = executablePath, - LineNumber = 0 // 需要通过其他方式获取行号 - }; - testList.Add(testInfo); - } - else if (line.EndsWith(".")) // 测试套件 - { - currentSuite = line.TrimEnd('.'); - } - } - - return testList; - } - } -} diff --git a/NahidaProject-UnitTestAdapter/NahidaProject-UnitTestAdapter/NahidaUnitTestConstant.cs b/NahidaProject-UnitTestAdapter/NahidaProject-UnitTestAdapter/NahidaUnitTestConstant.cs deleted file mode 100644 index 2c222f13d5873eea70b80393d0ff3602f5b77437..0000000000000000000000000000000000000000 --- a/NahidaProject-UnitTestAdapter/NahidaProject-UnitTestAdapter/NahidaUnitTestConstant.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace NahidaProject_UnitTestAdapter -{ - public static class NahidaUnitTestConstant - { - public const string ExecutorUri = "executor://NahidaUnitTestExecutor"; - public const string FriendlyName = "NahidaProject C++ Test Adapter"; - } -} diff --git a/NahidaProject-UnitTestAdapter/NahidaProject-UnitTestAdapter/NahidaUnitTestData.cs b/NahidaProject-UnitTestAdapter/NahidaProject-UnitTestAdapter/NahidaUnitTestData.cs deleted file mode 100644 index 6e7b01c0e273677a31a84f5d02b39150755542c2..0000000000000000000000000000000000000000 --- a/NahidaProject-UnitTestAdapter/NahidaProject-UnitTestAdapter/NahidaUnitTestData.cs +++ /dev/null @@ -1,28 +0,0 @@ -namespace NahidaProject_UnitTestAdapter { - public class NahidaUnitTestData { - public string FullyQualifiedName { - get; - set; - } - - public string DisplayName { - get; - set; - - } - public string TestSuite { - get; - set; - } - - public string SourceFile { - get; - set; - } - - public int LineNumber { - get; - set; - } - } -} diff --git a/NahidaProject-UnitTestAdapter/NahidaProject-UnitTestAdapter/source.extension.vsixmanifest b/NahidaProject-UnitTestAdapter/NahidaProject-UnitTestAdapter/source.extension.vsixmanifest deleted file mode 100644 index 3b763de00ea5af4c69175d968ff8354bb62cc9a2..0000000000000000000000000000000000000000 --- a/NahidaProject-UnitTestAdapter/NahidaProject-UnitTestAdapter/source.extension.vsixmanifest +++ /dev/null @@ -1,22 +0,0 @@ - - - - - NahidaProject_UnitTestAdapter - Test adapter for NahidaProject unit tests.Test adapter for C++ unit tests - - - - amd64 - - - - - - - - - - - - diff --git a/NahidaProject4CSharp/.gitignore b/NahidaProject4CSharp/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..0582db01a9f04027888cefe1c7d17bb3f785b1ae --- /dev/null +++ b/NahidaProject4CSharp/.gitignore @@ -0,0 +1,4 @@ +### Microsoft Visual Studio ### +.vs +bin +obj \ No newline at end of file diff --git a/NahidaProject4CSharp/FunctionalTest/NahidaProject4CSharp.UnitTest.FunctionalTest/App.config b/NahidaProject4CSharp/FunctionalTest/NahidaProject4CSharp.UnitTest.FunctionalTest/App.config new file mode 100644 index 0000000000000000000000000000000000000000..193aecc675e7d4600fde2d37cf350bce7a056948 --- /dev/null +++ b/NahidaProject4CSharp/FunctionalTest/NahidaProject4CSharp.UnitTest.FunctionalTest/App.config @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/NahidaProject4CSharp/FunctionalTest/NahidaProject4CSharp.UnitTest.FunctionalTest/NahidaProject4CSharp.UnitTest.FunctionalTest.csproj b/NahidaProject4CSharp/FunctionalTest/NahidaProject4CSharp.UnitTest.FunctionalTest/NahidaProject4CSharp.UnitTest.FunctionalTest.csproj new file mode 100644 index 0000000000000000000000000000000000000000..e07cbf9f7a7f91cd3051fb55f52aed2b30c67986 --- /dev/null +++ b/NahidaProject4CSharp/FunctionalTest/NahidaProject4CSharp.UnitTest.FunctionalTest/NahidaProject4CSharp.UnitTest.FunctionalTest.csproj @@ -0,0 +1,60 @@ + + + + + latest + Debug + AnyCPU + {2DA61D72-1C90-4E5A-9FB3-69E1B5200F33} + Exe + NahidaProject4CSharp.UnitTest.FunctionalTest + NahidaProject4CSharp.UnitTest.FunctionalTest + v4.8 + 512 + true + true + + + AnyCPU + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + AnyCPU + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + + + + + + + + + + + + + + + {ac8f58ae-96fa-49a3-8b24-0674ddfef2f7} + NahidaProject4CSharp.UnitTest + + + + \ No newline at end of file diff --git a/NahidaProject4CSharp/FunctionalTest/NahidaProject4CSharp.UnitTest.FunctionalTest/Program.cs b/NahidaProject4CSharp/FunctionalTest/NahidaProject4CSharp.UnitTest.FunctionalTest/Program.cs new file mode 100644 index 0000000000000000000000000000000000000000..3ace5a22f8e4e49a716c96037af1259208d87766 --- /dev/null +++ b/NahidaProject4CSharp/FunctionalTest/NahidaProject4CSharp.UnitTest.FunctionalTest/Program.cs @@ -0,0 +1,150 @@ +using NahidaProject4CSharp.UnitTest.NahidaBenchmarkTest; +using NahidaProject4CSharp.UnitTest.NahidaMockTest; +using NahidaProject4CSharp.UnitTest.NahidaUnitTest; +using System; +using System.Reflection; +using System.Text; + +namespace NahidaProject4CSharp.UnitTest.FunctionalTest { + public class UnitTestTests{ + + [Test] public void TestPoint1() { + Assert.Equals(1 + 1, 2); + } + + [Test] public void TestPoint2() { + Assert.Equals(2 + 2, 22); + } + } + + [BenchmarkClass(Name = "TestClass1", Description = "Testing various string operations performance")] + public class TestClass1 { + [Benchmark(Iterations = 1000, Warmup = 10, Description = "String concatenation using + operator")] + public void TestPoint1(){ + string result = ""; + for (int i = 0; i < 100; i++) + { + result += "test"; + } + } + + [Benchmark(Iterations = 1000, Warmup = 10, Description = "String concatenation using StringBuilder")] + public void TestPoint2(){ + var sb = new StringBuilder(); + for (int i = 0; i < 100; i++){ + sb.Append("test"); + } + sb.ToString(); + } + + [Benchmark(Iterations = 1000, Warmup = 10, Description = "String.Join operation")] + public void TestPoint3(){ + var array = new string[100]; + for (int i = 0; i < 100; i++){ + array[i] = "test"; + } + string.Join("", array); + } + + [Benchmark(Iterations = 100, Warmup = 5, Description = "Parameterized string format test")] + public void TestPoint4([BenchmarkParam("Hello", "World", "Test")] string input){ + string.Format("Formatted: {0}", input); + } + } + + public interface IUserService + { + string GetUserName(int userId); + int GetUserCount(); + void SaveUser(string name); + bool DeleteUser(int userId); + } + + public class UserServiceConsumer + { + private readonly IUserService _userService; + + public UserServiceConsumer(IUserService userService) + { + _userService = userService; + } + + public string GetWelcomeMessage(int userId) + { + var userName = _userService.GetUserName(userId); + return $"Welcome, {userName}!"; + } + + public void CreateUser(string name) + { + _userService.SaveUser(name); + } + + public bool RemoveUser(int userId) + { + return _userService.DeleteUser(userId); + } + } + + internal class Program { + static void Main() { + var assemblyRunner = new TestRunner(); + assemblyRunner.RunTests(Assembly.GetExecutingAssembly()); + assemblyRunner.PrintResults(); + + var runner = new BenchmarkRunner(); + + runner.OnTestCompleted += (result) =>{ + if (result.Success){ + Console.WriteLine($"Completed: {result.TestName} - {result.AverageMs:F3}ms avg"); + } + else{ + Console.WriteLine($"Failed: {result.TestName} - {result.Exception?.Message}"); + } + }; + + runner.OnSuiteCompleted += (suiteResult) =>{ + Console.WriteLine($"\nSuite '{suiteResult.SuiteName}' completed in {suiteResult.TotalDuration.TotalMilliseconds:F2}ms"); + }; + + var stringResults = runner.Run(); + BenchmarkReporter.GenerateDetailedConsoleReport(stringResults); + + var mockUserService = MockFactory.CreateMock(); + mockUserService.Setup(x => x.GetUserName(1), "John Doe"); + mockUserService.Setup(x => x.GetUserCount(), 5); + mockUserService.Setup(x => x.DeleteUser(1), true); + + var consumer = new UserServiceConsumer(mockUserService.Object); + + var welcomeMessage = consumer.GetWelcomeMessage(1); + Console.WriteLine($"Welcome Message: {welcomeMessage}"); + + consumer.CreateUser("Jane Doe"); + + var deleteResult = consumer.RemoveUser(1); + Console.WriteLine($"Delete Result: {deleteResult}"); + + try + { + mockUserService.Verify(x => x.GetUserName(1), Times.Once()); + mockUserService.Verify(x => x.SaveUser("Jane Doe"), Times.Once()); + mockUserService.Verify(x => x.DeleteUser(1), Times.Once()); + Console.WriteLine("All verifications passed!"); + } + catch (MockException ex) + { + Console.WriteLine($"Verification failed: {ex.Message}"); + } + + try + { + mockUserService.Verify(x => x.GetUserCount(), Times.Once()); + } + catch (MockException ex) + { + Console.WriteLine($"Expected verification failure: {ex.Message}"); + } + } + } +} diff --git a/NahidaProject-UnitTestAdapter/NahidaProject-UnitTestAdapter/Properties/AssemblyInfo.cs b/NahidaProject4CSharp/FunctionalTest/NahidaProject4CSharp.UnitTest.FunctionalTest/Properties/AssemblyInfo.cs similarity index 31% rename from NahidaProject-UnitTestAdapter/NahidaProject-UnitTestAdapter/Properties/AssemblyInfo.cs rename to NahidaProject4CSharp/FunctionalTest/NahidaProject4CSharp.UnitTest.FunctionalTest/Properties/AssemblyInfo.cs index 349aea24046826a6b7576c5c51948c0c60d33b96..007413798dd3aa0f8f3ba636c2ca9bdd32357a20 100644 --- a/NahidaProject-UnitTestAdapter/NahidaProject-UnitTestAdapter/Properties/AssemblyInfo.cs +++ b/NahidaProject4CSharp/FunctionalTest/NahidaProject4CSharp.UnitTest.FunctionalTest/Properties/AssemblyInfo.cs @@ -2,32 +2,32 @@ using System.Runtime.CompilerServices; using System.Runtime.InteropServices; -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyTitle("NahidaProject_UnitTestAdapter")] +// 有关程序集的一般信息由以下 +// 控制。更改这些特性值可修改 +// 与程序集关联的信息。 +[assembly: AssemblyTitle("NahidaProject4CSharp.UnitTest.FunctionalTest")] [assembly: AssemblyDescription("")] [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("NahidaProject_UnitTestAdapter")] -[assembly: AssemblyCopyright("")] +[assembly: AssemblyProduct("NahidaProject4CSharp.UnitTest.FunctionalTest")] +[assembly: AssemblyCopyright("Copyright © 2025")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. +// 将 ComVisible 设置为 false 会使此程序集中的类型 +//对 COM 组件不可见。如果需要从 COM 访问此程序集中的类型 +//请将此类型的 ComVisible 特性设置为 true。 [assembly: ComVisible(false)] -// Version information for an assembly consists of the following four values: +// 如果此项目向 COM 公开,则下列 GUID 用于类型库的 ID +[assembly: Guid("2da61d72-1c90-4e5a-9fb3-69e1b5200f33")] + +// 程序集的版本信息由下列四个值组成: // -// Major Version -// Minor Version -// Build Number -// Revision +// 主版本 +// 次版本 +// 生成号 +// 修订号 // -// You can specify all the values or you can default the Build and Revision Numbers -// by using the '*' as shown below: -// [assembly: AssemblyVersion("1.0.*")] [assembly: AssemblyVersion("1.0.0.0")] [assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/NahidaProject4CSharp/NahidaProject4CSharp.UnitTest/NahidaBenchmarkTest.cs b/NahidaProject4CSharp/NahidaProject4CSharp.UnitTest/NahidaBenchmarkTest.cs new file mode 100644 index 0000000000000000000000000000000000000000..23ab6c28b4c0d84dc76f0aab63bbf43a90cea645 --- /dev/null +++ b/NahidaProject4CSharp/NahidaProject4CSharp.UnitTest/NahidaBenchmarkTest.cs @@ -0,0 +1,370 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Reflection; +using System.Text; + +namespace NahidaProject4CSharp.UnitTest.NahidaBenchmarkTest +{ + + [AttributeUsage(AttributeTargets.Method)] + public class BenchmarkAttribute : Attribute + { + public int Iterations { get; set; } = 1; + public int Warmup { get; set; } = 0; + public string Description { get; set; } = ""; + } + + [AttributeUsage(AttributeTargets.Class)] + public class BenchmarkClassAttribute : Attribute + { + public string Name { get; set; } = ""; + public string Description { get; set; } = ""; + } + + [AttributeUsage(AttributeTargets.Parameter)] + public class BenchmarkParamAttribute : Attribute + { + public object[] Values { get; set; } + + public BenchmarkParamAttribute(params object[] values) + { + Values = values; + } + } + public class BenchmarkResult + { + public string TestName { get; set; } + public string Description { get; set; } + public TimeSpan Duration { get; set; } + public long Ticks { get; set; } + public double Milliseconds { get; set; } + public Dictionary Parameters { get; set; } = new Dictionary(); + public Exception Exception { get; set; } + public bool Success { get; set; } + public long MemoryBefore { get; set; } + public long MemoryAfter { get; set; } + public long MemoryUsed { get; set; } + public int Iterations { get; set; } + public double AverageMs { get; set; } + + public override string ToString() + { + if (!Success) + { + return $"{TestName}: FAILED - {Exception?.Message}"; + } + + return $"{TestName}: {AverageMs:F3}ms avg ({Iterations} iterations)"; + } + } + public class BenchmarkSuiteResult + { + public string SuiteName { get; set; } + public string Description { get; set; } + public List Results { get; set; } = new List(); + public DateTime StartTime { get; set; } + public DateTime EndTime { get; set; } + public TimeSpan TotalDuration => EndTime - StartTime; + + public void AddResult(BenchmarkResult result) + { + Results.Add(result); + } + + public IEnumerable SuccessfulResults => Results.Where(r => r.Success); + public IEnumerable FailedResults => Results.Where(r => !r.Success); + } + + public class BenchmarkRunner + { + private readonly List _suiteResults = new List(); + + public event Action OnTestCompleted; + public event Action OnSuiteCompleted; + + public BenchmarkSuiteResult Run() where T : new() + { + return Run(typeof(T)); + } + + public BenchmarkSuiteResult Run(Type benchmarkType) + { + var suiteResult = new BenchmarkSuiteResult + { + StartTime = DateTime.Now + }; + + var classAttr = benchmarkType.GetCustomAttribute(); + suiteResult.SuiteName = !string.IsNullOrEmpty(classAttr?.Name) ? classAttr.Name : benchmarkType.Name; + suiteResult.Description = classAttr?.Description ?? ""; + + Console.WriteLine($"Starting benchmark suite: {suiteResult.SuiteName}"); + if (!string.IsNullOrEmpty(suiteResult.Description)) + { + Console.WriteLine($"Description: {suiteResult.Description}"); + } + Console.WriteLine(new string('=', 60)); + + var instance = Activator.CreateInstance(benchmarkType); + + var methods = benchmarkType.GetMethods(BindingFlags.Public | BindingFlags.Instance).Where(m => m.GetCustomAttribute() != null); + + foreach (var method in methods) + { + var attr = method.GetCustomAttribute(); + var testName = method.Name; + var description = attr.Description; + + var parameters = method.GetParameters(); + if (parameters.Length == 0) + { + var result = RunTestMethod(instance, method, attr, testName, description); + suiteResult.AddResult(result); + OnTestCompleted?.Invoke(result); + } + else + { + RunParameterizedTest(instance, method, attr, suiteResult); + } + } + + suiteResult.EndTime = DateTime.Now; + _suiteResults.Add(suiteResult); + OnSuiteCompleted?.Invoke(suiteResult); + + return suiteResult; + } + + private void RunParameterizedTest(object instance, MethodInfo method, BenchmarkAttribute attr, BenchmarkSuiteResult suiteResult) + { + var parameters = method.GetParameters(); + var parameterValues = new List(); + + GenerateParameterCombinations(parameters, 0, new object[parameters.Length], parameterValues); + + foreach (var paramValues in parameterValues) + { + var testName = $"{method.Name}({string.Join(", ", paramValues.Select(v => v?.ToString() ?? "null"))})"; + var result = RunTestMethod(instance, method, attr, testName, attr.Description, paramValues); + suiteResult.AddResult(result); + OnTestCompleted?.Invoke(result); + } + } + + private void GenerateParameterCombinations(ParameterInfo[] parameters, int index, object[] currentValues, List combinations) + { + if (index >= parameters.Length) + { + combinations.Add((object[])currentValues.Clone()); + return; + } + + var param = parameters[index]; + var paramAttr = param.GetCustomAttribute(); + + if (paramAttr != null && paramAttr.Values.Length > 0) + { + foreach (var value in paramAttr.Values) + { + var convertedValue = ConvertValue(value, param.ParameterType); + currentValues[index] = convertedValue; + GenerateParameterCombinations(parameters, index + 1, currentValues, combinations); + } + } + else + { + currentValues[index] = GetDefaultValue(param.ParameterType); + GenerateParameterCombinations(parameters, index + 1, currentValues, combinations); + } + } + + private object ConvertValue(object value, Type targetType) + { + if (value == null) return null; + if (targetType.IsAssignableFrom(value.GetType())) return value; + + try + { + return Convert.ChangeType(value, targetType); + } + catch + { + return value; + } + } + + private object GetDefaultValue(Type type) + { + if (type.IsValueType) + { + return Activator.CreateInstance(type); + } + return null; + } + + private BenchmarkResult RunTestMethod(object instance, MethodInfo method, BenchmarkAttribute attr, string testName, string description, object[] parameters = null) + { + var result = new BenchmarkResult + { + TestName = testName, + Description = description, + Iterations = attr.Iterations + }; + + try + { + for (int i = 0; i < attr.Warmup; i++) + { + method.Invoke(instance, parameters); + } + + GC.Collect(); + GC.WaitForPendingFinalizers(); + GC.Collect(); + + var memoryBefore = GC.GetTotalMemory(false); + var stopwatch = Stopwatch.StartNew(); + + for (int i = 0; i < attr.Iterations; i++) + { + method.Invoke(instance, parameters); + } + + stopwatch.Stop(); + + var memoryAfter = GC.GetTotalMemory(false); + + result.Duration = stopwatch.Elapsed; + result.Ticks = stopwatch.ElapsedTicks; + result.Milliseconds = stopwatch.Elapsed.TotalMilliseconds; + result.MemoryBefore = memoryBefore; + result.MemoryAfter = memoryAfter; + result.MemoryUsed = Math.Max(0, memoryAfter - memoryBefore); + result.AverageMs = result.Milliseconds / attr.Iterations; + result.Success = true; + + if (parameters != null) + { + var methodParams = method.GetParameters(); + for (int i = 0; i < parameters.Length; i++) + { + result.Parameters[methodParams[i].Name] = parameters[i]; + } + } + } + catch (Exception ex) + { + result.Exception = ex.InnerException ?? ex; + result.Success = false; + } + + return result; + } + + public List RunAllTest() + { + var results = new List(); + + // 查找所有基准测试类 + var assemblies = AppDomain.CurrentDomain.GetAssemblies(); + foreach (var assembly in assemblies) + { + try + { + var benchmarkTypes = assembly.GetTypes().Where(t => t.GetCustomAttribute() != null); + + foreach (var type in benchmarkTypes) + { + var result = Run(type); + results.Add(result); + } + } + catch (ReflectionTypeLoadException reflectionTypeLoadException) + { + Console.WriteLine(reflectionTypeLoadException.Message); + } + } + + return results; + } + } + + public class BenchmarkReporter + { + public static void GenerateConsoleReport(BenchmarkSuiteResult suiteResult) + { + Console.WriteLine($"\nBenchmark Suite: {suiteResult.SuiteName}"); + if (!string.IsNullOrEmpty(suiteResult.Description)) + { + Console.WriteLine($"Description: {suiteResult.Description}"); + } + Console.WriteLine($"Total Duration: {suiteResult.TotalDuration.TotalMilliseconds:F2}ms"); + Console.WriteLine(new string('-', 60)); + + foreach (var result in suiteResult.SuccessfulResults.OrderBy(r => r.AverageMs)) + { + Console.WriteLine($"[SUCCESS] {result}"); + if (!string.IsNullOrEmpty(result.Description)) + { + Console.WriteLine($" Description: {result.Description}"); + } + Console.WriteLine($" Memory: {result.MemoryUsed / 1024.0:F2} KB"); + if (result.Parameters.Count > 0) + { + Console.WriteLine($" Parameters: {string.Join(", ", result.Parameters.Select(p => $"{p.Key}={p.Value}"))}"); + } + Console.WriteLine(); + } + + if (suiteResult.FailedResults.Any()) + { + Console.WriteLine("Failed Tests:"); + Console.WriteLine(new string('-', 30)); + foreach (var result in suiteResult.FailedResults) + { + Console.WriteLine($"[FAILURE] {result.TestName}: {result.Exception?.Message}"); + } + } + + Console.WriteLine(new string('=', 60)); + } + + public static void GenerateDetailedConsoleReport(BenchmarkSuiteResult suiteResult) + { + GenerateConsoleReport(suiteResult); + + if (suiteResult.SuccessfulResults.Any()) + { + Console.WriteLine("\nPerformance Summary:"); + Console.WriteLine(new string('-', 40)); + + var results = suiteResult.SuccessfulResults.OrderBy(r => r.AverageMs).ToList(); + var fastest = results.First(); + var slowest = results.Last(); + + Console.WriteLine($"Fastest: {fastest.TestName} ({fastest.AverageMs:F3}ms)"); + Console.WriteLine($"Slowest: {slowest.TestName} ({slowest.AverageMs:F3}ms)"); + Console.WriteLine($"Performance Range: {slowest.AverageMs - fastest.AverageMs:F3}ms"); + + if (results.Count > 1) + { + var average = results.Average(r => r.AverageMs); + Console.WriteLine($"Average: {average:F3}ms"); + } + } + } + + public static string GenerateCSVReport(BenchmarkSuiteResult suiteResult) { + var sb = new StringBuilder(); + sb.AppendLine("TestName,Description,DurationMs,AverageMs,Iterations,MemoryUsedKB,Success"); + + foreach (var result in suiteResult.Results){ + sb.AppendLine($"{result.TestName}," + $"{result.Description}," + $"{result.Milliseconds:F6}," + $"{result.AverageMs:F6}," + $"{result.Iterations}," + $"{result.MemoryUsed / 1024.0:F2}," + $"{result.Success}"); + } + + return sb.ToString(); + } + } +} diff --git a/NahidaProject4CSharp/NahidaProject4CSharp.UnitTest/NahidaMockTest.cs b/NahidaProject4CSharp/NahidaProject4CSharp.UnitTest/NahidaMockTest.cs new file mode 100644 index 0000000000000000000000000000000000000000..b755af3daf0a5ae61154d1211a2b419fcb6580c0 --- /dev/null +++ b/NahidaProject4CSharp/NahidaProject4CSharp.UnitTest/NahidaMockTest.cs @@ -0,0 +1,174 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; + +namespace NahidaProject4CSharp.UnitTest.NahidaMockTest { + public interface IMock where T : class + { + T Object { get; } + void Setup(Expression> expression, TResult returnValue); + void Setup(Expression> expression); + void Verify(Expression> expression, Times times); + void Verify(Expression> expression, Times times); + } + + public class MockException : Exception + { + public MockException(string message) : base(message) + { + } + + public MockException(string message, Exception innerException) : base(message, innerException) + { + } + } + + public class Times + { + public int Count { get; private set; } + public string Description { get; private set; } + + private Times(int count, string description) + { + Count = count; + Description = description; + } + + public static Times Once() => new Times(1, "Once"); + public static Times Never() => new Times(0, "Never"); + public static Times AtLeastOnce() => new Times(-1, "AtLeastOnce"); + public static Times Exactly(int count) => new Times(count, $"Exactly {count}"); + } + + public class Invocation + { + public string MethodName { get; set; } + public object[] Arguments { get; set; } + public Type ReturnType { get; set; } + } + + public class Mock : IMock where T : class + { + private readonly T _mockObject; + private readonly Dictionary _setups; + private readonly List _invocations; + + public T Object => _mockObject; + + public Mock() + { + _setups = new Dictionary(); + _invocations = new List(); + _mockObject = CreateProxy(); + } + + private T CreateProxy() + { + var type = typeof(T); + var proxyType = CreateProxyType(type); + return (T)Activator.CreateInstance(proxyType, this); + } + + private Type CreateProxyType(Type interfaceType) + { + var typeName = $"Mock_{interfaceType.Name}_{Guid.NewGuid():N}"; + return interfaceType; + } + + public void Setup(Expression> expression, TResult returnValue) + { + var methodName = GetMethodName(expression); + var key = GetMethodKey(methodName, expression.Parameters.ToArray()); + _setups[key] = returnValue; + } + + public void Setup(Expression> expression) + { + var methodName = GetMethodName(expression); + var key = GetMethodKey(methodName, expression.Parameters.ToArray()); + _setups[key] = null; + } + + public void Verify(Expression> expression, Times times) + { + var methodName = GetMethodName(expression); + var key = GetMethodKey(methodName, expression.Parameters.ToArray()); + var count = _invocations.Count(i => i.MethodName == key); + + if (!IsCountValid(count, times)) + { + throw new MockException($"Expected invocation on the mock {times.Description}, but was {count} times."); + } + } + + public void Verify(Expression> expression, Times times) + { + var methodName = GetMethodName(expression); + var key = GetMethodKey(methodName, expression.Parameters.ToArray()); + var count = _invocations.Count(i => i.MethodName == key); + + if (!IsCountValid(count, times)) + { + throw new MockException($"Expected invocation on the mock {times.Description}, but was {count} times."); + } + } + + private string GetMethodName(LambdaExpression expression) + { + if (expression.Body is MethodCallExpression methodCall) + { + return methodCall.Method.Name; + } + throw new ArgumentException("Expression must be a method call"); + } + + private string GetMethodKey(string methodName, ParameterExpression[] parameters) + { + return $"{methodName}_{parameters.Length}"; + } + + private bool IsCountValid(int actualCount, Times expectedTimes) + { + return expectedTimes.Count switch + { + 0 => actualCount == 0, + 1 => actualCount == 1, + -1 => actualCount >= 1, + _ => actualCount == expectedTimes.Count + }; + } + + internal object HandleInvocation(string methodName, Type returnType, params object[] args) + { + var key = $"{methodName}_{args.Length}"; + _invocations.Add(new Invocation + { + MethodName = key, + Arguments = args, + ReturnType = returnType + }); + + if (_setups.ContainsKey(key)) + { + return _setups[key]; + } + + if (returnType == typeof(void)) + { + return null; + } + + return returnType.IsValueType ? Activator.CreateInstance(returnType) : null; + } + } + + public static class MockFactory + { + public static Mock CreateMock() where T : class + { + return new Mock(); + } + } + +} diff --git a/NahidaProject-UnitTestAdapter/NahidaProject-UnitTestAdapter/NahidaProject-UnitTestAdapter.csproj b/NahidaProject4CSharp/NahidaProject4CSharp.UnitTest/NahidaProject4CSharp.UnitTest.csproj similarity index 36% rename from NahidaProject-UnitTestAdapter/NahidaProject-UnitTestAdapter/NahidaProject-UnitTestAdapter.csproj rename to NahidaProject4CSharp/NahidaProject4CSharp.UnitTest/NahidaProject4CSharp.UnitTest.csproj index 4f9ec96d48fc723d5fe6f600d90d65c3792a0eaf..cfd12b4e09ce3c7b24593c161febfe31bb752b10 100644 --- a/NahidaProject-UnitTestAdapter/NahidaProject-UnitTestAdapter/NahidaProject-UnitTestAdapter.csproj +++ b/NahidaProject4CSharp/NahidaProject4CSharp.UnitTest/NahidaProject4CSharp.UnitTest.csproj @@ -1,31 +1,18 @@ - - - 17.0 - $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) - + + latest Debug AnyCPU - 2.0 - {82b43b9b-a64c-4715-b499-d71e9ca2bd60};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} - {CACAF0CF-99F5-4922-A9BA-822AEFC1FA6D} + {AC8F58AE-96FA-49A3-8B24-0674DDFEF2F7} Library Properties - NahidaProject_UnitTestAdapter - NahidaProject-UnitTestAdapter - v4.7.2 - true - true - true - false - false - true - true - Program - $(DevEnvDir)devenv.exe - /rootsuffix Exp + NahidaProject4CSharp.UnitTest + NahidaProject4CSharp.UnitTest + v4.8 + 512 + true true @@ -44,35 +31,24 @@ prompt 4 - - - - - - - - - - Designer - - + + false + + + + + + + + - - 17.14.1 - - - + + + + - - \ No newline at end of file diff --git a/NahidaProject4CSharp/NahidaProject4CSharp.UnitTest/NahidaUnitTest.cs b/NahidaProject4CSharp/NahidaProject4CSharp.UnitTest/NahidaUnitTest.cs new file mode 100644 index 0000000000000000000000000000000000000000..3d7c9fcde8d306914664f08aac96e0ba7e48d61d --- /dev/null +++ b/NahidaProject4CSharp/NahidaProject4CSharp.UnitTest/NahidaUnitTest.cs @@ -0,0 +1,176 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Reflection; + +namespace NahidaProject4CSharp.UnitTest.NahidaUnitTest { + [AttributeUsage(AttributeTargets.Method)] + public class TestAttribute : Attribute{ + + } + + public class AssertionException : Exception{ + public AssertionException(string message) : base(message){ + + } + } + + public static class Assert { + public static void AreEqual(object expected, object actual, string message = "") { + if (!Equals(expected, actual)) { + throw new AssertionException($"Expected: {expected}, Actual: {actual}. {message}"); + } + } + + public static void AreNotEqual(object expected, object actual, string message = "") { + if (Equals(expected, actual)) { + throw new AssertionException($"Expected not equal to: {expected}, Actual: {actual}. {message}"); + } + } + + public static void IsTrue(bool condition, string message = ""){ + if (!condition){ + throw new AssertionException($"Expected true, but was false. {message}"); + } + } + + public static void IsFalse(bool condition, string message = ""){ + if (condition){ + throw new AssertionException($"Expected false, but was true. {message}"); + } + } + + public static void IsNull(object value, string message = ""){ + if (value != null){ + throw new AssertionException($"Expected null, but was {value}. {message}"); + } + } + + public static void IsNotNull(object value, string message = ""){ + if (value == null){ + throw new AssertionException($"Expected not null. {message}"); + } + } + + public static void Fail(string message = ""){ + throw new AssertionException($"Test failed. {message}"); + } + } + + public class TestResult{ + public string TestName { get; set; } + public bool Passed { get; set; } + public string Message { get; set; } + public double Duration { get; set; } + + public TestResult(string testName, bool passed, string message = "", double duration = 0) { + TestName = testName; + Passed = passed; + Message = message; + Duration = duration; + } + } + + public class TestRunner { + private readonly List _results = new List(); + + public List RunTests(Assembly assembly){ + _results.Clear(); + var testClasses = GetTestClasses(assembly); + + foreach (var testClass in testClasses){ + RunTestClass(testClass); + } + return _results; + } + + public List RunTests() where T : class, new(){ + _results.Clear(); + RunTestClass(typeof(T)); + return _results; + } + + private List GetTestClasses(Assembly assembly){ + var testClasses = new List(); + + foreach (var type in assembly.GetTypes()){ + if (HasTestMethods(type)){ + testClasses.Add(type); + } + } + + return testClasses; + } + + private bool HasTestMethods(Type type){ + var methods = type.GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly); + foreach (var method in methods){ + if (method.GetCustomAttribute() != null){ + return true; + } + } + return false; + } + + private void RunTestClass(Type testClassType){ + var methods = testClassType.GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly); + + foreach (var method in methods){ + var testAttribute = method.GetCustomAttribute(); + if (testAttribute != null){ + RunTestMethod(testClassType, method); + } + } + } + + private void RunTestMethod(Type testClassType, MethodInfo method){ + var testName = $"{testClassType.Name}.{method.Name}"; + var stopwatch = Stopwatch.StartNew(); + + try{ + var instance = Activator.CreateInstance(testClassType); + method.Invoke(instance, null); + + stopwatch.Stop(); + _results.Add(new TestResult(testName, true, "", stopwatch.Elapsed.TotalMilliseconds)); + } + catch (TargetInvocationException ex){ + stopwatch.Stop(); + var innerException = ex.InnerException; + if (innerException is AssertionException assertionEx) + { + _results.Add(new TestResult(testName, false, assertionEx.Message, stopwatch.Elapsed.TotalMilliseconds)); + } + else + { + _results.Add(new TestResult(testName, false, $"Exception: {innerException?.Message}", stopwatch.Elapsed.TotalMilliseconds)); + } + } + catch (Exception ex){ + stopwatch.Stop(); + _results.Add(new TestResult(testName, false, $"Unexpected error: {ex.Message}", stopwatch.Elapsed.TotalMilliseconds)); + } + } + + public void PrintResults(){ + Console.WriteLine("Test Results:"); + Console.WriteLine("============="); + + int passed = 0, failed = 0; + + foreach (var result in _results){ + if (result.Passed){ + Console.WriteLine($"[SUCCESS] {result.TestName} (Passed) - {result.Duration:F2}ms"); + passed++; + } + else{ + Console.WriteLine($"[FAILURE] {result.TestName} (Failed) - {result.Duration:F2}ms"); + Console.WriteLine($" Message: {result.Message}"); + failed++; + } + } + + Console.WriteLine($"\nSummary: {passed} passed, {failed} failed, {_results.Count} total"); + } + } +} diff --git a/NahidaProject4CSharp/NahidaProject4CSharp.UnitTest/Properties/AssemblyInfo.cs b/NahidaProject4CSharp/NahidaProject4CSharp.UnitTest/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000000000000000000000000000000000..82761952db77e47d1bb668956023aae079340439 --- /dev/null +++ b/NahidaProject4CSharp/NahidaProject4CSharp.UnitTest/Properties/AssemblyInfo.cs @@ -0,0 +1,33 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// 有关程序集的一般信息由以下 +// 控制。更改这些特性值可修改 +// 与程序集关联的信息。 +[assembly: AssemblyTitle("NahidaProject4CSharp.UnitTest")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("")] +[assembly: AssemblyCopyright("")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// 将 ComVisible 设置为 false 会使此程序集中的类型 +//对 COM 组件不可见。如果需要从 COM 访问此程序集中的类型 +//请将此类型的 ComVisible 特性设置为 true。 +[assembly: ComVisible(true)] + +// 如果此项目向 COM 公开,则下列 GUID 用于类型库的 ID +[assembly: Guid("ac8f58ae-96fa-49a3-8b24-0674ddfef2f7")] + +// 程序集的版本信息由下列四个值组成: +// +// 主版本 +// 次版本 +// 生成号 +// 修订号 +// +[assembly: AssemblyVersion("0.0.0.0")] +[assembly: AssemblyFileVersion("0.0.0.0")] diff --git a/NahidaProject-UnitTestAdapter/NahidaProject-UnitTestAdapter.sln b/NahidaProject4CSharp/NahidaProject4CSharp.sln similarity index 35% rename from NahidaProject-UnitTestAdapter/NahidaProject-UnitTestAdapter.sln rename to NahidaProject4CSharp/NahidaProject4CSharp.sln index ac745cd0ee9559bda7141a45ff3065a94108fc74..3225aa40fd7319eed1fe0e5270d1ff98fb8550f6 100644 --- a/NahidaProject-UnitTestAdapter/NahidaProject-UnitTestAdapter.sln +++ b/NahidaProject4CSharp/NahidaProject4CSharp.sln @@ -3,35 +3,34 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.14.36414.22 d17.14 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NahidaProject-UnitTestAdapter", "NahidaProject-UnitTestAdapter\NahidaProject-UnitTestAdapter.csproj", "{CACAF0CF-99F5-4922-A9BA-822AEFC1FA6D}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NahidaProject4CSharp.UnitTest", "NahidaProject4CSharp.UnitTest\NahidaProject4CSharp.UnitTest.csproj", "{AC8F58AE-96FA-49A3-8B24-0674DDFEF2F7}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "FunctionalTest", "FunctionalTest", "{02EA681E-C7D8-13C7-8484-4AC65E1B71E8}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NahidaProject4CSharp.UnitTest.FunctionalTest", "FunctionalTest\NahidaProject4CSharp.UnitTest.FunctionalTest\NahidaProject4CSharp.UnitTest.FunctionalTest.csproj", "{2DA61D72-1C90-4E5A-9FB3-69E1B5200F33}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU - Debug|arm64 = Debug|arm64 - Debug|x86 = Debug|x86 Release|Any CPU = Release|Any CPU - Release|arm64 = Release|arm64 - Release|x86 = Release|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {CACAF0CF-99F5-4922-A9BA-822AEFC1FA6D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {CACAF0CF-99F5-4922-A9BA-822AEFC1FA6D}.Debug|Any CPU.Build.0 = Debug|Any CPU - {CACAF0CF-99F5-4922-A9BA-822AEFC1FA6D}.Debug|arm64.ActiveCfg = Debug|arm64 - {CACAF0CF-99F5-4922-A9BA-822AEFC1FA6D}.Debug|arm64.Build.0 = Debug|arm64 - {CACAF0CF-99F5-4922-A9BA-822AEFC1FA6D}.Debug|x86.ActiveCfg = Debug|x86 - {CACAF0CF-99F5-4922-A9BA-822AEFC1FA6D}.Debug|x86.Build.0 = Debug|x86 - {CACAF0CF-99F5-4922-A9BA-822AEFC1FA6D}.Release|Any CPU.ActiveCfg = Release|Any CPU - {CACAF0CF-99F5-4922-A9BA-822AEFC1FA6D}.Release|Any CPU.Build.0 = Release|Any CPU - {CACAF0CF-99F5-4922-A9BA-822AEFC1FA6D}.Release|arm64.ActiveCfg = Release|arm64 - {CACAF0CF-99F5-4922-A9BA-822AEFC1FA6D}.Release|arm64.Build.0 = Release|arm64 - {CACAF0CF-99F5-4922-A9BA-822AEFC1FA6D}.Release|x86.ActiveCfg = Release|x86 - {CACAF0CF-99F5-4922-A9BA-822AEFC1FA6D}.Release|x86.Build.0 = Release|x86 + {AC8F58AE-96FA-49A3-8B24-0674DDFEF2F7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AC8F58AE-96FA-49A3-8B24-0674DDFEF2F7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AC8F58AE-96FA-49A3-8B24-0674DDFEF2F7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AC8F58AE-96FA-49A3-8B24-0674DDFEF2F7}.Release|Any CPU.Build.0 = Release|Any CPU + {2DA61D72-1C90-4E5A-9FB3-69E1B5200F33}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2DA61D72-1C90-4E5A-9FB3-69E1B5200F33}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2DA61D72-1C90-4E5A-9FB3-69E1B5200F33}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2DA61D72-1C90-4E5A-9FB3-69E1B5200F33}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {2DA61D72-1C90-4E5A-9FB3-69E1B5200F33} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} + EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {E50F05FC-D142-4B7E-8F52-E9C418235E9E} + SolutionGuid = {3C6716E8-851E-4719-A112-8F33471CFFA2} EndGlobalSection EndGlobal