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
-