From adc095d97447b30c2e0188b9dcff2707c6d2a55c Mon Sep 17 00:00:00 2001 From: David Federman Date: Tue, 27 Jan 2026 18:27:42 -0800 Subject: [PATCH] Upgrade to .NET 10 --- .github/workflows/ci.yml | 2 +- .github/workflows/pr.yml | 2 +- Directory.Packages.props | 23 +-- ZWave.NET.sln | 5 +- global.json | 14 +- .../CommandClassFactoryGenerator.cs | 154 ++++++++---------- src/ZWave.BuildTools/ConfigGeneratorBase.cs | 110 ++++++------- src/ZWave.Server/ZWave.Server.csproj | 2 +- src/ZWave.Tests/AssertExtensions.cs | 2 +- .../Serial/Commands/CommandTestBase.cs | 2 +- src/ZWave.Tests/Serial/FrameParserTests.cs | 4 +- src/ZWave.Tests/Serial/FrameTests.cs | 22 +-- src/ZWave.Tests/ZWave.Tests.csproj | 21 +-- src/ZWave/ZWave.csproj | 3 +- 14 files changed, 166 insertions(+), 200 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 66df5a7..0fd5dee 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,7 +21,7 @@ jobs: - name: Build run: dotnet build --configuration Release --no-restore - name: Test - run: dotnet test --configuration Release --no-build --logger trx --results-directory TestResults --collect:"XPlat Code Coverage" + run: dotnet test --configuration Release --no-build --report-trx --results-directory TestResults --coverage - name: Upload test results uses: actions/upload-artifact@v4 with: diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 1b41809..75f132a 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -19,7 +19,7 @@ jobs: - name: Build run: dotnet build --configuration Release --no-restore - name: Test - run: dotnet test --configuration Release --no-build --logger trx --results-directory TestResults --collect:"XPlat Code Coverage" + run: dotnet test --configuration Release --no-build --report-trx --results-directory TestResults --coverage - name: Upload test results uses: actions/upload-artifact@v4 with: diff --git a/Directory.Packages.props b/Directory.Packages.props index 4e80d65..c2fb163 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -1,21 +1,16 @@ - - - - - - - - - - - - - + + + + + + + + - + \ No newline at end of file diff --git a/ZWave.NET.sln b/ZWave.NET.sln index b55b157..faa7bb4 100644 --- a/ZWave.NET.sln +++ b/ZWave.NET.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.1.31911.260 +# Visual Studio Version 18 +VisualStudioVersion = 18.4.11426.133 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{DDBF7DBF-FA50-4311-A52A-8AC978CD417A}" EndProject @@ -15,6 +15,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution .gitignore = .gitignore Directory.Build.props = Directory.Build.props Directory.Build.rsp = Directory.Build.rsp + Directory.Packages.props = Directory.Packages.props global.json = global.json LICENSE = LICENSE NuGet.config = NuGet.config diff --git a/global.json b/global.json index ad8ad01..5ddbd87 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,12 @@ { - "sdk": { - "version": "8.0.201", - "rollForward": "latestFeature" - } + "sdk": { + "version": "10.0.102", + "rollForward": "latestFeature" + }, + "msbuild-sdks": { + "MSTest.Sdk": "4.0.2" + }, + "test": { + "runner": "Microsoft.Testing.Platform" + } } \ No newline at end of file diff --git a/src/ZWave.BuildTools/CommandClassFactoryGenerator.cs b/src/ZWave.BuildTools/CommandClassFactoryGenerator.cs index d019de7..43f0868 100644 --- a/src/ZWave.BuildTools/CommandClassFactoryGenerator.cs +++ b/src/ZWave.BuildTools/CommandClassFactoryGenerator.cs @@ -1,11 +1,12 @@ -using System.Text; +using System.Collections.Immutable; +using System.Text; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Syntax; namespace ZWave.BuildTools; [Generator] -public sealed class CommandClassFactoryGenerator : ISourceGenerator +public sealed class CommandClassFactoryGenerator : IIncrementalGenerator { private static readonly DiagnosticDescriptor DuplicateCommandClassId = new DiagnosticDescriptor( id: "ZWAVE001", @@ -15,9 +16,7 @@ public sealed class CommandClassFactoryGenerator : ISourceGenerator DiagnosticSeverity.Error, isEnabledByDefault: true); - public void Initialize(GeneratorInitializationContext context) - { - const string attributeSource = @" + private const string AttributeSource = @" namespace ZWave.CommandClasses; [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)] @@ -29,21 +28,71 @@ public CommandClassAttribute(CommandClassId id) => (Id) = (id); } "; - context.RegisterForPostInitialization((pi) => pi.AddSource("CommandClassAttribute.generated.cs", attributeSource)); - context.RegisterForSyntaxNotifications(() => new SyntaxReceiver()); - } - public void Execute(GeneratorExecutionContext context) + public void Initialize(IncrementalGeneratorInitializationContext context) { - SyntaxReceiver syntaxReceiver = (SyntaxReceiver)context.SyntaxReceiver!; + context.RegisterPostInitializationOutput(static ctx => + ctx.AddSource("CommandClassAttribute.generated.cs", AttributeSource)); + + // Find all classes with [CommandClass] and extract (CommandClassId, ClassName) + IncrementalValuesProvider<(string CommandClassId, string ClassName)> commandClasses = context.SyntaxProvider + .ForAttributeWithMetadataName( + "ZWave.CommandClasses.CommandClassAttribute", + predicate: static (node, _) => node is ClassDeclarationSyntax, + transform: static (ctx, _) => + { + string className = ((ClassDeclarationSyntax)ctx.TargetNode).Identifier.ToString(); + + // Get the attribute argument as "CommandClassId.EnumMemberName" + foreach (AttributeData attr in ctx.Attributes) + { + if (attr.ConstructorArguments.Length > 0) + { + TypedConstant arg = attr.ConstructorArguments[0]; + if (arg.Type?.TypeKind == TypeKind.Enum) + { + // Get enum member name from the constant value + string? memberName = arg.Type.GetMembers() + .OfType() + .FirstOrDefault(f => f.HasConstantValue && Equals(f.ConstantValue, arg.Value)) + ?.Name; - foreach (Diagnostic diagnostic in syntaxReceiver.DiagnosticsToReport) + if (memberName != null) + { + return ($"CommandClassId.{memberName}", className); + } + } + } + } + + return default; + }) + .Where(static x => x.Item1 != null!); + + IncrementalValueProvider> collected = commandClasses.Collect(); + + context.RegisterSourceOutput(collected, static (spc, items) => { - context.ReportDiagnostic(diagnostic); - } + var idToType = new Dictionary(StringComparer.Ordinal); + + foreach ((string commandClassId, string className) in items) + { + if (idToType.ContainsKey(commandClassId)) + { + spc.ReportDiagnostic(Diagnostic.Create(DuplicateCommandClassId, Location.None, commandClassId)); + } + else + { + idToType.Add(commandClassId, className); + } + } - Dictionary commandClassIdToType = syntaxReceiver.CommandClassIdToType; + spc.AddSource("CommandClassFactory.generated.cs", GenerateSource(idToType)); + }); + } + private static string GenerateSource(Dictionary idToType) + { var sb = new StringBuilder(); sb.Append(@" #nullable enable @@ -56,18 +105,9 @@ internal static class CommandClassFactory { "); - foreach (KeyValuePair pair in commandClassIdToType) + foreach (KeyValuePair pair in idToType) { - string commandClassId = pair.Key; - string commandClassType = pair.Value; - - // { CommandClassId.Basic, (info, driver, node) => new BasicCommandClass(info, driver, node) }, - sb.Append(" { "); - sb.Append(commandClassId); - sb.Append(", (info, driver, node) => new "); - sb.Append(commandClassType); - sb.Append("(info, driver, node) },"); - sb.AppendLine(); + sb.AppendLine($" {{ {pair.Key}, (info, driver, node) => new {pair.Value}(info, driver, node) }},"); } sb.Append(@" }; @@ -76,18 +116,9 @@ internal static class CommandClassFactory { "); - foreach (KeyValuePair pair in commandClassIdToType) + foreach (KeyValuePair pair in idToType) { - string commandClassId = pair.Key; - string commandClassType = pair.Value; - - // { typeof(BasicCommandClass), CommandClassId.Basic }, - sb.Append(" { typeof("); - sb.Append(commandClassType); - sb.Append("), "); - sb.Append(commandClassId); - sb.Append(" },"); - sb.AppendLine(); + sb.AppendLine($" {{ typeof({pair.Value}), {pair.Key} }},"); } sb.Append(@" }; @@ -102,55 +133,6 @@ public static CommandClassId GetCommandClassId() => TypeToIdMap[typeof(TCommandClass)]; } "); - - context.AddSource("CommandClassFactory.generated.cs", sb.ToString()); + return sb.ToString(); } - - private sealed class SyntaxReceiver : ISyntaxReceiver - { - public Dictionary CommandClassIdToType { get; } = new Dictionary(StringComparer.Ordinal); - - public List DiagnosticsToReport { get; } = new List(); - - public void OnVisitSyntaxNode(SyntaxNode syntaxNode) - { - if (syntaxNode is ClassDeclarationSyntax classDeclarationSyntax) - { - foreach (AttributeListSyntax attributeListSyntax in classDeclarationSyntax.AttributeLists) - { - foreach (AttributeSyntax attributeSyntax in attributeListSyntax.Attributes) - { - var attributeName = attributeSyntax.Name.ToString(); - if (attributeName == "CommandClass" - || attributeName == "CommandClassAttribute") - { - if (attributeSyntax.ArgumentList != null - && attributeSyntax.ArgumentList.Arguments.Count > 0) - { - AttributeArgumentSyntax attributeArgumentSyntax = attributeSyntax.ArgumentList.Arguments[0]; - string attributeArgumentValue = attributeArgumentSyntax.ToString(); - if (attributeArgumentValue.StartsWith("CommandClassId.")) - { - if (CommandClassIdToType.ContainsKey(attributeArgumentValue)) - { - var diagnostic = Diagnostic.Create( - DuplicateCommandClassId, - attributeSyntax.GetLocation(), - attributeArgumentValue); - DiagnosticsToReport.Add(diagnostic); - } - else - { - CommandClassIdToType.Add(attributeArgumentValue, classDeclarationSyntax.Identifier.ToString()); - } - } - } - } - } - } - } - } - } - - private record struct CommandClassMetadata(string CommandClassId, string CommandClassType); } diff --git a/src/ZWave.BuildTools/ConfigGeneratorBase.cs b/src/ZWave.BuildTools/ConfigGeneratorBase.cs index e2a9fcb..516bb51 100644 --- a/src/ZWave.BuildTools/ConfigGeneratorBase.cs +++ b/src/ZWave.BuildTools/ConfigGeneratorBase.cs @@ -1,10 +1,12 @@ -using System.Text.Json; +using System.Collections.Immutable; +using System.Text.Json; using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Text; namespace ZWave.BuildTools; -public abstract class ConfigGeneratorBase : ISourceGenerator +public abstract class ConfigGeneratorBase : IIncrementalGenerator { private static readonly DiagnosticDescriptor MissingConfig = new DiagnosticDescriptor( id: "ZWAVE002", @@ -22,80 +24,68 @@ public abstract class ConfigGeneratorBase : ISourceGenerator DiagnosticSeverity.Error, isEnabledByDefault: true); + private static readonly JsonSerializerOptions JsonOptions = new JsonSerializerOptions + { + AllowTrailingCommas = true, + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + ReadCommentHandling = JsonCommentHandling.Skip, + }; + protected abstract string ConfigType { get; } - public void Initialize(GeneratorInitializationContext context) + public void Initialize(IncrementalGeneratorInitializationContext context) { - // No initialization required - } + // Filter additional texts to find the matching config file and extract path + content + IncrementalValueProvider> configFiles = context.AdditionalTextsProvider + .Combine(context.AnalyzerConfigOptionsProvider) + .Where(pair => pair.Right.GetOptions(pair.Left).TryGetValue("build_metadata.additionalfiles.ConfigType", out string? configType) && configType.Equals(ConfigType)) + .Select(static (pair, cancellationToken) => (Path: pair.Left.Path, Content: pair.Left.GetText(cancellationToken)?.ToString() ?? string.Empty)) + .Where(static item => !string.IsNullOrEmpty(item.Content)) + .Collect(); - public void Execute(GeneratorExecutionContext context) -{ - AdditionalText? configFile = GetMatchingConfigFile(context); - if (configFile == null) + context.RegisterSourceOutput(configFiles, (sourceProductionContext, files) => { - context.ReportDiagnostic(Diagnostic.Create(MissingConfig, Location.None, ConfigType)); - return; - } - - string configContent = configFile.GetText()!.ToString(); + if (files.Length == 0) + { + sourceProductionContext.ReportDiagnostic(Diagnostic.Create(MissingConfig, Location.None, ConfigType)); + return; + } - TConfig? config; - try - { - config = JsonSerializer.Deserialize( - configContent, - new JsonSerializerOptions - { - AllowTrailingCommas = true, - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - ReadCommentHandling = JsonCommentHandling.Skip, - }); - } - catch (JsonException ex) - { - Location location; - if (ex.LineNumber == null || ex.BytePositionInLine == null) + var configFile = files[0]; + TConfig? config; + try { - location = Location.None; + config = JsonSerializer.Deserialize(configFile.Content, JsonOptions); } - else + catch (JsonException ex) { - var linePosition = new LinePosition((int)ex.LineNumber.Value, (int)ex.BytePositionInLine.Value); + Location location; + if (ex.LineNumber == null || ex.BytePositionInLine == null) + { + location = Location.None; + } + else + { + var linePosition = new LinePosition((int)ex.LineNumber.Value, (int)ex.BytePositionInLine.Value); var lineSpan = new LinePositionSpan(linePosition, linePosition); var textSpan = new TextSpan(0, 0); location = Location.Create(configFile.Path, textSpan, lineSpan); - } - - var diagnostic = Diagnostic.Create(InvalidConfig, location, ConfigType, ex.Message); - context.ReportDiagnostic(diagnostic); - return; - } - - if (config == null) - { - var diagnostic = Diagnostic.Create(InvalidConfig, Location.None, ConfigType, "Json was invalid"); - context.ReportDiagnostic(diagnostic); - return; - } - - string source = CreateSource(config); - context.AddSource(ConfigType + ".generated.cs", source); - } + } - protected abstract string CreateSource(TConfig config); + var diagnostic = Diagnostic.Create(InvalidConfig, location, ConfigType, ex.Message); + sourceProductionContext.ReportDiagnostic(diagnostic); + return; + } - private AdditionalText? GetMatchingConfigFile(GeneratorExecutionContext context) - { - foreach (AdditionalText file in context.AdditionalFiles) - { - if (context.AnalyzerConfigOptions.GetOptions(file).TryGetValue("build_metadata.additionalfiles.ConfigType", out string? configType) - && configType.Equals(ConfigType)) + if (config == null) { - return file; + sourceProductionContext.ReportDiagnostic(Diagnostic.Create(InvalidConfig, Location.None, ConfigType, "Json was invalid")); + return; } - } - return null; + sourceProductionContext.AddSource(ConfigType + ".generated.cs", CreateSource(config)); + }); } + + protected abstract string CreateSource(TConfig config); } diff --git a/src/ZWave.Server/ZWave.Server.csproj b/src/ZWave.Server/ZWave.Server.csproj index 67b60f8..c2a0259 100644 --- a/src/ZWave.Server/ZWave.Server.csproj +++ b/src/ZWave.Server/ZWave.Server.csproj @@ -1,7 +1,7 @@ - net8.0 + net10.0 diff --git a/src/ZWave.Tests/AssertExtensions.cs b/src/ZWave.Tests/AssertExtensions.cs index d29de31..1a93322 100644 --- a/src/ZWave.Tests/AssertExtensions.cs +++ b/src/ZWave.Tests/AssertExtensions.cs @@ -114,7 +114,7 @@ public static void ObjectsAreEquivalentInternal( PropertyInfo[] actualProperties = actualType.GetProperties(bindingFlags) .Where(p => !excludedProperties.Any(excludedProperty => p.Name.Equals(excludedProperty, StringComparison.OrdinalIgnoreCase))) .ToArray(); - Assert.AreEqual(expectedProperties.Length, actualProperties.Length, $"Object property counts do not match for object {propertyPathBase ?? ""}"); + Assert.HasCount(expectedProperties.Length, actualProperties, $"Object property counts do not match for object {propertyPathBase ?? ""}"); var properties = new List<(PropertyInfo ExpectedProperty, PropertyInfo ActualProperty)>(actualProperties.Length); foreach (PropertyInfo expectedProperty in expectedProperties) diff --git a/src/ZWave.Tests/Serial/Commands/CommandTestBase.cs b/src/ZWave.Tests/Serial/Commands/CommandTestBase.cs index e3becc6..7c9768f 100644 --- a/src/ZWave.Tests/Serial/Commands/CommandTestBase.cs +++ b/src/ZWave.Tests/Serial/Commands/CommandTestBase.cs @@ -36,7 +36,7 @@ internal static void TestReceivableCommand( if (commandId == 0) { - Assert.ThrowsException(() => TCommand.CommandId); + Assert.Throws(() => TCommand.CommandId); } else { diff --git a/src/ZWave.Tests/Serial/FrameParserTests.cs b/src/ZWave.Tests/Serial/FrameParserTests.cs index 1b326b9..bc8cefb 100644 --- a/src/ZWave.Tests/Serial/FrameParserTests.cs +++ b/src/ZWave.Tests/Serial/FrameParserTests.cs @@ -37,7 +37,7 @@ public void ParseInvalidDataOnly() Assert.AreEqual(default, frame); } - [DataTestMethod] + [TestMethod] [DataRow(FrameHeader.ACK)] [DataRow(FrameHeader.NAK)] [DataRow(FrameHeader.CAN)] @@ -107,7 +107,7 @@ public void ParseDataFrame() Assert.AreEqual(FrameType.Data, frame.Type); } - [DataTestMethod] + [TestMethod] [DataRow( new byte[] { diff --git a/src/ZWave.Tests/Serial/FrameTests.cs b/src/ZWave.Tests/Serial/FrameTests.cs index b6c0f8d..0ebed57 100644 --- a/src/ZWave.Tests/Serial/FrameTests.cs +++ b/src/ZWave.Tests/Serial/FrameTests.cs @@ -19,10 +19,10 @@ public class FrameTests [TestMethod] public void ConstructorEmptyData() { - Assert.ThrowsException(() => new Frame(Array.Empty())); + Assert.Throws(() => new Frame(Array.Empty())); } - [DataTestMethod] + [TestMethod] [DataRow(FrameHeader.ACK, FrameType.ACK)] [DataRow(FrameHeader.NAK, FrameType.NAK)] [DataRow(FrameHeader.CAN, FrameType.CAN)] @@ -32,17 +32,17 @@ public void ConstructorSingleByteFrames(byte frameHeader, FrameType expectedFram Assert.AreEqual(expectedFrameType, frame.Type); } - [DataTestMethod] + [TestMethod] [DataRow(FrameHeader.ACK)] [DataRow(FrameHeader.NAK)] [DataRow(FrameHeader.CAN)] public void ConstructorSingleByteFramesWithExtraData(byte frameHeader) { - Assert.ThrowsException(() => new Frame(new byte[] { frameHeader, 0x01 })); + Assert.Throws(() => new Frame(new byte[] { frameHeader, 0x01 })); } // Single byte frames use a singleton array to avoid holding onto many small arrays - [DataTestMethod] + [TestMethod] [DataRow(true, new[] { FrameHeader.ACK })] [DataRow(true, new[] { FrameHeader.NAK })] [DataRow(true, new[] { FrameHeader.CAN })] @@ -80,7 +80,7 @@ public void ConstructorDataFrameWithInvalidLength() byte[] frameData = ValidDataFrameData.ToArray(); frameData[1]--; // Length - Assert.ThrowsException(() => new Frame(frameData)); + Assert.Throws(() => new Frame(frameData)); } [TestMethod] @@ -94,7 +94,7 @@ public void ConstructorUnknownHeader() 0xEF, }; - Assert.ThrowsException(() => new Frame(frameData)); + Assert.Throws(() => new Frame(frameData)); } [TestMethod] @@ -117,10 +117,10 @@ public void ToDataFrameForDataFrame() [TestMethod] public void ToDataFrameForNonDataFrame() { - Assert.ThrowsException(() => Frame.ACK.ToDataFrame()); + Assert.Throws(() => Frame.ACK.ToDataFrame()); } - [DataTestMethod] + [TestMethod] [DataRow(true, new[] { FrameHeader.ACK }, new[] { FrameHeader.ACK })] [DataRow(true, new[] { FrameHeader.NAK }, new[] { FrameHeader.NAK })] [DataRow(true, new[] { FrameHeader.CAN }, new[] { FrameHeader.CAN })] @@ -170,7 +170,7 @@ public void Equality(bool expectedAreEqual, byte[] frameData1, byte[] frameData2 Assert.AreEqual(expectedAreEqual, frame1 == frame2); } - [DataTestMethod] + [TestMethod] [DataRow(new[] { FrameHeader.ACK })] [DataRow(new[] { FrameHeader.NAK })] [DataRow(new[] { FrameHeader.CAN })] @@ -225,6 +225,6 @@ void AddHashCode(Frame frame) 0xF6 // Checksum })); - Assert.AreEqual(hashCodesAdded, hashCodes.Count); + Assert.HasCount(hashCodesAdded, hashCodes); } } \ No newline at end of file diff --git a/src/ZWave.Tests/ZWave.Tests.csproj b/src/ZWave.Tests/ZWave.Tests.csproj index 54846ec..0c42aa0 100644 --- a/src/ZWave.Tests/ZWave.Tests.csproj +++ b/src/ZWave.Tests/ZWave.Tests.csproj @@ -1,26 +1,19 @@ - + - net8.0 - false + net10.0 - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - + - + + Microsoft.VisualStudio.TestTools.UnitTesting.ExecutionScope.MethodLevel + true + diff --git a/src/ZWave/ZWave.csproj b/src/ZWave/ZWave.csproj index a7599dd..b61ca3b 100644 --- a/src/ZWave/ZWave.csproj +++ b/src/ZWave/ZWave.csproj @@ -1,12 +1,11 @@  - net8.0 + net10.0 -