diff --git a/Samples/DumpVariables_DumpSessionInfo/DumpVariables_DumpSessionInfo.csproj b/Samples/DumpVariables_DumpSessionInfo/DumpVariables_DumpSessionInfo.csproj index 727d1a7..b60d884 100644 --- a/Samples/DumpVariables_DumpSessionInfo/DumpVariables_DumpSessionInfo.csproj +++ b/Samples/DumpVariables_DumpSessionInfo/DumpVariables_DumpSessionInfo.csproj @@ -15,7 +15,7 @@ - + diff --git a/Samples/LocationAndWarnings/LocationAndWarnings.csproj b/Samples/LocationAndWarnings/LocationAndWarnings.csproj index 727d1a7..b60d884 100644 --- a/Samples/LocationAndWarnings/LocationAndWarnings.csproj +++ b/Samples/LocationAndWarnings/LocationAndWarnings.csproj @@ -15,7 +15,7 @@ - + diff --git a/Samples/MinimalExample/MinimalExample.csproj b/Samples/MinimalExample/MinimalExample.csproj index 727d1a7..b60d884 100644 --- a/Samples/MinimalExample/MinimalExample.csproj +++ b/Samples/MinimalExample/MinimalExample.csproj @@ -15,7 +15,7 @@ - + diff --git a/Samples/SpeedRPMGear/SpeedRPMGear.csproj b/Samples/SpeedRPMGear/SpeedRPMGear.csproj index 3c9f825..772f0f3 100644 --- a/Samples/SpeedRPMGear/SpeedRPMGear.csproj +++ b/Samples/SpeedRPMGear/SpeedRPMGear.csproj @@ -17,7 +17,7 @@ - + diff --git a/Sdk/SVappsLAB.iRacingTelemetrySDK.CodeGen/SVappsLAB.iRacingTelemetrySDK.CodeGen.csproj b/Sdk/SVappsLAB.iRacingTelemetrySDK.CodeGen/SVappsLAB.iRacingTelemetrySDK.CodeGen.csproj index 8e7e76c..63180c1 100644 --- a/Sdk/SVappsLAB.iRacingTelemetrySDK.CodeGen/SVappsLAB.iRacingTelemetrySDK.CodeGen.csproj +++ b/Sdk/SVappsLAB.iRacingTelemetrySDK.CodeGen/SVappsLAB.iRacingTelemetrySDK.CodeGen.csproj @@ -9,7 +9,7 @@ - + diff --git a/Sdk/SVappsLAB.iRacingTelemetrySDK.EnumsAndFlags/TelemetryClient_Flags.cs b/Sdk/SVappsLAB.iRacingTelemetrySDK.EnumsAndFlags/TelemetryClient_Flags.cs index 63fea5c..91a07d4 100644 --- a/Sdk/SVappsLAB.iRacingTelemetrySDK.EnumsAndFlags/TelemetryClient_Flags.cs +++ b/Sdk/SVappsLAB.iRacingTelemetrySDK.EnumsAndFlags/TelemetryClient_Flags.cs @@ -59,6 +59,7 @@ public enum SessionFlags Serviceable = 0x00040000, // car is allowed service (not a flag) Furled = 0x00080000, Repair = 0x00100000, + DqScoringInvalid = 0x00200000, // car is disqualified and scoring is disabled // start lights StartHidden = 0x10000000, diff --git a/Sdk/SVappsLAB.iRacingTelemetrySDK/DataProviders/DataProviderBase.cs b/Sdk/SVappsLAB.iRacingTelemetrySDK/DataProviders/DataProviderBase.cs index 52a9e80..e31c003 100644 --- a/Sdk/SVappsLAB.iRacingTelemetrySDK/DataProviders/DataProviderBase.cs +++ b/Sdk/SVappsLAB.iRacingTelemetrySDK/DataProviders/DataProviderBase.cs @@ -1,12 +1,12 @@ /** * Copyright (C) 2024-2026 Scott Velez - * + * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -120,9 +120,8 @@ public string GetSessionInfoYaml() var offSet = header.sessionInfoOffset; var maxLen = header.sessionInfoLen; - var span = new Span(_dataPtr + offSet, maxLen); - var sessInfo = ExtractNullTerminatedString(span, maxLen); - return sessInfo; + var span = new ReadOnlySpan(_dataPtr + offSet, maxLen); + return SessionInfoDecoder.Decode(span); } public object? GetVarValue(string varName) @@ -198,6 +197,42 @@ protected void CopyNewTelemetryDataToBuffer(int recNum = 0) ros.CopyTo(_telemetryDataBuffer); } + // live telemetry can be overwritten by the sim while we read it. mirror the + // official sdk's torn-read detection: 'tickCountBegin' is updated before a + // write starts and 'tickCount' after it completes. if the tickCount read + // before the copy matches tickCountBegin after, no write was in progress + protected bool TryCopyLiveTelemetryDataToBuffer(out int validTickCount) + { + const int MAX_ATTEMPTS = 2; + + // try a few times to get the data out + for (var attempt = 0; attempt < MAX_ATTEMPTS; attempt++) + { + var header = GetHeader(); + var bufIndex = header.GetMostRecentBufferIndex(); + var varBuf = header.GetVarBuf(bufIndex); + + var curTickCount = varBuf.tickCount; + Thread.MemoryBarrier(); + + var ros = new ReadOnlySpan(_dataPtr + varBuf.bufOffset, header.bufLen); + ros.CopyTo(_telemetryDataBuffer); + + Thread.MemoryBarrier(); + + // re-read from shared memory to see if a write was in progress + if (curTickCount == GetHeader().GetVarBuf(bufIndex).tickCountBegin) + { + validTickCount = curTickCount; + return true; + } + } + + // the data changed out from under us + validTickCount = 0; + return false; + } + VarHeaderDictionary ReadVarHeaders() { var ros = new ReadOnlySpan(_dataPtr + _header.varHeaderOffset, _header.numVars); diff --git a/Sdk/SVappsLAB.iRacingTelemetrySDK/DataProviders/LiveDataProvider.cs b/Sdk/SVappsLAB.iRacingTelemetrySDK/DataProviders/LiveDataProvider.cs index 93214ba..a032a8e 100644 --- a/Sdk/SVappsLAB.iRacingTelemetrySDK/DataProviders/LiveDataProvider.cs +++ b/Sdk/SVappsLAB.iRacingTelemetrySDK/DataProviders/LiveDataProvider.cs @@ -19,7 +19,6 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; -using SVappsLAB.iRacingTelemetrySDK.irSDKDefines; namespace SVappsLAB.iRacingTelemetrySDK.DataProviders { @@ -58,7 +57,13 @@ public override Task WaitForDataReady(TimeSpan timeSpan, CancellationToken internal bool ProcessNewData() { - var latestTickCount = GetLatestVarBuff().tickCount; + // copy new data to the access buffer, + // validating data is good and no write was in progress + if (!TryCopyLiveTelemetryDataToBuffer(out var latestTickCount)) + { + _logger.LogWarning("data changed while we were reading it. skipping this sample"); + return false; + } // if we missed any telemetry data, log that it happened if (latestTickCount > _lastTickCount) @@ -67,7 +72,7 @@ internal bool ProcessNewData() if (_lastTickCount != 0 && tickDiff > 0) { _dataDropCount += tickDiff; - _logger.LogWarning("dropped {count} data records. {total} total. last tick: {lastTick}, current tick: {currentTick}", tickDiff, _dataDropCount, _lastTickCount, latestTickCount); + _logger.LogWarning("dropped {count} data records. a total of {total} missed so far. last tick: {lastTick}, current tick: {currentTick}", tickDiff, _dataDropCount, _lastTickCount, latestTickCount); } } @@ -78,28 +83,11 @@ internal bool ProcessNewData() _logger.LogDebug("new data is older than our last sample. lost connection? will resync"); } - // copy new data to the access buffer for later reading - CopyNewTelemetryDataToBuffer(); // resync - update our last tick count _lastTickCount = latestTickCount; return true; } - - irsdk_varBuf GetLatestVarBuff() - { - var header = GetHeader(); - - var vb = header.varBuf1; - if (header.varBuf2.tickCount > vb.tickCount) - vb = header.varBuf2; - if (header.varBuf3.tickCount > vb.tickCount) - vb = header.varBuf3; - if (header.varBuf4.tickCount > vb.tickCount) - vb = header.varBuf4; - return vb; - - } public override ValueTask DisposeAsync() { if (_dataReadyEvent != null) diff --git a/Sdk/SVappsLAB.iRacingTelemetrySDK/DataProviders/SessionInfoDecoder.cs b/Sdk/SVappsLAB.iRacingTelemetrySDK/DataProviders/SessionInfoDecoder.cs new file mode 100644 index 0000000..57825b7 --- /dev/null +++ b/Sdk/SVappsLAB.iRacingTelemetrySDK/DataProviders/SessionInfoDecoder.cs @@ -0,0 +1,61 @@ +/** + * Copyright (C) 2024-2026 Scott Velez + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. +**/ + +using System; +using System.Text; + +namespace SVappsLAB.iRacingTelemetrySDK.DataProviders +{ + // Decodes the raw session-info bytes from the memory-mapped file / IBT file + // into a string, depending on the WeekendInfo:Encoding: tag (UTF8). + internal static class SessionInfoDecoder + { + private static readonly byte[] EncodingKey = Encoding.ASCII.GetBytes("Encoding:"); + private static readonly byte[] Utf8Value = Encoding.ASCII.GetBytes("UTF8"); + + /// + /// Decode using the encoding declared by the WeekendInfo:Encoding: tag + /// Older session info without "UTF8" use ISO-8859-1 + /// + public static string Decode(ReadOnlySpan raw) + { + // trim at the first null terminator + var nullIdx = raw.IndexOf((byte)0); + if (nullIdx >= 0) + raw = raw.Slice(0, nullIdx); + + if (IsUtf8(raw)) + return Encoding.UTF8.GetString(raw); + + return Encoding.Latin1.GetString(raw); + } + + public static bool IsUtf8(ReadOnlySpan raw) + { + var keyIdx = raw.IndexOf(EncodingKey); + if (keyIdx < 0) + return false; + + // the value is on the same line, immediately after the key + var value = raw.Slice(keyIdx + EncodingKey.Length); + var eol = value.IndexOf((byte)'\n'); + if (eol >= 0) + value = value.Slice(0, eol); + + return value.IndexOf(Utf8Value) >= 0; + } + } +} diff --git a/Sdk/SVappsLAB.iRacingTelemetrySDK/Models/DriverInfo.cs b/Sdk/SVappsLAB.iRacingTelemetrySDK/Models/DriverInfo.cs index ed81a4b..b0bb07d 100644 --- a/Sdk/SVappsLAB.iRacingTelemetrySDK/Models/DriverInfo.cs +++ b/Sdk/SVappsLAB.iRacingTelemetrySDK/Models/DriverInfo.cs @@ -24,6 +24,7 @@ public class DriverInfo public int DriverCarIdx { get; set; } // 0 public int DriverUserID { get; set; } // 22176 public int PaceCarIdx { get; set; } // -1 + public int DriverIsAdmin { get; set; } // 0/1 - the player is an admin and can have access to the admin chat commands public float DriverHeadPosX { get; set; } // in units of length (mm, m, etc.) public float DriverHeadPosY { get; set; } // in units of length (mm, m, etc.) public float DriverHeadPosZ { get; set; } // in units of length (mm, m, etc.) @@ -38,6 +39,9 @@ public class DriverInfo public int DriverCarGearNumForward { get; set; } // 6 public int DriverCarGearNeutral { get; set; } // 1 public int DriverCarGearReverse { get; set; } // 1 + public string DriverGearboxType { get; set; } // Sequential/H-Pattern/Automatic/Unknown - transmission type in virtual car + public string DriverGearboxControlType { get; set; } // Sequential/H-Pattern/H-Pattern Direct/Automatic/Unknown - physical shifter type player is using + public string DriverCarShiftAid { get; set; } // Manual/Antistall/Antistall_Clutch/Antistall_Clutch_Throttle/Automatic - type of shift aids the driver has turned on public float DriverCarSLFirstRPM { get; set; } // 5600.000 public float DriverCarSLShiftRPM { get; set; } // 7200.000 public float DriverCarSLLastRPM { get; set; } // 7200.000 @@ -51,12 +55,8 @@ public class DriverInfo public int DriverSetupPassedTech { get; set; } // 1 public int DriverIncidentCount { get; set; } // 0 public float DriverBrakeCurvingFactor { get; set; } // 0.001 - public int DriverIsAdmin { get; set; } // 0/1 - the player is an admin and can have access to the admin chat commands - public string DriverGearboxType { get; set; } // Sequential/H-Pattern/Automatic/Unknown - transmission type in virtual car - public string DriverGearboxControlType { get; set; } // Sequential/H-Pattern/H-Pattern Direct/Automatic/Unknown - physical shifter type player is using - public string DriverCarShiftAid { get; set; } // Manual/Antistall/Antistall_Clutch/Antistall_Clutch_Throttle/Automatic - type of shift aids the driver has turned on - public List Drivers { get; set; } public List DriverTires { get; set; } + public List Drivers { get; set; } } public class Driver diff --git a/Sdk/SVappsLAB.iRacingTelemetrySDK/Models/WeekendInfo.cs b/Sdk/SVappsLAB.iRacingTelemetrySDK/Models/WeekendInfo.cs index c59b20a..80ffd2f 100644 --- a/Sdk/SVappsLAB.iRacingTelemetrySDK/Models/WeekendInfo.cs +++ b/Sdk/SVappsLAB.iRacingTelemetrySDK/Models/WeekendInfo.cs @@ -1,12 +1,12 @@ /** * Copyright (C) 2024-2026 Scott Velez - * + * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -21,6 +21,7 @@ namespace SVappsLAB.iRacingTelemetrySDK { public class WeekendInfo { + public string Encoding { get; set; } // "UTF8" public string TrackName { get; set; } // spa up public int TrackID { get; set; } // 143 public string TrackLength { get; set; } // 6.93 km (10cm accuracy) @@ -116,7 +117,7 @@ public class WeekendOptions public string IncidentWarningInitialLimit { get; set; } // 'unlimited' or '0'..'n public object IncidentWarningSubsequentLimit { get; set; } // 'unlimited' or '0'..'n' public string FastRepairsLimit { get; set; } // 'unlimited' or '0'..'n' - public string GreenWhiteCheckeredLimit { get; set; } //'unlimited' or '0'..'n' + public string GreenWhiteCheckeredLimit { get; set; } //'unlimited' or '0'..'n' } diff --git a/Sdk/SVappsLAB.iRacingTelemetrySDK/SVappsLAB.iRacingTelemetrySDK.csproj b/Sdk/SVappsLAB.iRacingTelemetrySDK/SVappsLAB.iRacingTelemetrySDK.csproj index c5be267..5e3248e 100644 --- a/Sdk/SVappsLAB.iRacingTelemetrySDK/SVappsLAB.iRacingTelemetrySDK.csproj +++ b/Sdk/SVappsLAB.iRacingTelemetrySDK/SVappsLAB.iRacingTelemetrySDK.csproj @@ -27,9 +27,9 @@ - - - + + + @@ -47,7 +47,7 @@ iRacing iRacingSDK irsdk IBT Telemetry SDK API NUGET.md LICENSE - 2.0.0 + 2.1.0 Scott Velez SVappsLAB Copyright (c) 2026 SVappsLAB diff --git a/Sdk/SVappsLAB.iRacingTelemetrySDK/irSDK_defines.cs b/Sdk/SVappsLAB.iRacingTelemetrySDK/irSDK_defines.cs index ab9614f..d682667 100644 --- a/Sdk/SVappsLAB.iRacingTelemetrySDK/irSDK_defines.cs +++ b/Sdk/SVappsLAB.iRacingTelemetrySDK/irSDK_defines.cs @@ -1,12 +1,12 @@ /** * Copyright (C) 2024-2026 Scott Velez - * + * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -24,7 +24,15 @@ internal static class Constants { public const int IRSDK_MAX_BUFS = 4; public const int IRSDK_MAX_STRING = 32; + // descriptions can be longer than max_string! public const int IRSDK_MAX_DESC = 64; + + // define markers for unlimited session lap and time + public const int IRSDK_UNLIMITED_LAPS = 32767; + public const float IRSDK_UNLIMITED_TIME = 604800.0f; + + // latest version of our telemetry headers + public const int IRSDK_VER = 2; } internal enum irsdk_VarType : Int32 @@ -47,28 +55,33 @@ internal enum irsdk_VarType : Int32 [StructLayout(LayoutKind.Sequential, Pack = 4)] internal struct irsdk_varBuf { - public int tickCount; - public int bufOffset; - public int pad1; - public int pad2; + public int tickCount; // used to detect changes in data (updated AFTER write completes) + public int bufOffset; // offset from header + public int tickCountBegin; // updated BEFORE write starts (for torn read detection) + public int pad; // (16 byte align) } [StructLayout(LayoutKind.Sequential, Pack = 4)] - internal struct irsdk_header + internal unsafe struct irsdk_header { - public int ver; - public irsdk_StatusField status; - public int tickRate; - public int sessionInfoUpdate; - public int sessionInfoLen; - public int sessionInfoOffset; - public int numVars; - public int varHeaderOffset; - public int numBuf; - public int bufLen; - // padding - public int pad1; - public int pad2; + public int ver; // this api header version, see IRSDK_VER + public irsdk_StatusField status;// bitfield using irsdk_StatusField + public int tickRate; // ticks per second (60 or 360 etc) + + // session information, updated periodically + public int sessionInfoUpdate; // Incremented when session info changes + public int sessionInfoLen; // Length in bytes of session info string + public int sessionInfoOffset; // Session info, encoded in YAML format + + // State data, output at tickRate + public int numVars; // length of array pointed to by varHeaderOffset + public int varHeaderOffset; // offset to irsdk_varHeader[numVars] array + + public int numBuf; // <= IRSDK_MAX_BUFS (3 for now) + public int bufLen; // length in bytes for one line + public int curBufTickCount; // stashed copy of the current tickCount, can read this to see if new data is available + public byte curBuf; // index of the most recently written buffer (0 to IRSDK_MAX_BUFS-1) + public fixed byte pad1[3]; // 16 byte align // if we don't use an array here. allows us to read this structure directly from unmanaged memory public irsdk_varBuf varBuf1; @@ -77,17 +90,27 @@ internal struct irsdk_header public irsdk_varBuf varBuf4; #region methods - public irsdk_varBuf GetMostRecentBuffer() + public irsdk_varBuf GetMostRecentBuffer() => GetVarBuf(GetMostRecentBufferIndex()); + + public int GetMostRecentBufferIndex() { - var vb = varBuf1; - if (varBuf2.tickCount > vb.tickCount) - vb = varBuf2; - if (varBuf3.tickCount > vb.tickCount) - vb = varBuf3; - if (varBuf4.tickCount > vb.tickCount) - vb = varBuf4; - return vb; + // use curBuf to find the most recently written buffer + int index = curBuf; + if (index >= Math.Min(numBuf, Constants.IRSDK_MAX_BUFS)) + index = 0; + return index; } + + // varBuf is exposed as discrete fields (so the header can be read directly + // from unmanaged memory). this helper provides access + public irsdk_varBuf GetVarBuf(int index) => index switch + { + 0 => varBuf1, + 1 => varBuf2, + 2 => varBuf3, + 3 => varBuf4, + _ => throw new ArgumentOutOfRangeException(nameof(index)), + }; #endregion } diff --git a/Sdk/tests/SmokeTests/Base/Base.Models.cs b/Sdk/tests/SmokeTests/Base/Base.Models.cs index bcbd6a7..cf09815 100644 --- a/Sdk/tests/SmokeTests/Base/Base.Models.cs +++ b/Sdk/tests/SmokeTests/Base/Base.Models.cs @@ -30,28 +30,29 @@ protected async Task BaseVerifyModelMatchesRawYaml(ITelemetryClient? missingProperties = null; - var rawSessionTask = Task.Run(async () => - { - await foreach (var rawYaml in client.SessionDataYaml) + await client.Monitor( + new TelemetryHandlers { - sessionInfoReceived = true; - - var allMissingProperties = ValidateModelAgainstYaml(rawYaml); + OnRawSessionInfoUpdate = rawYaml => + { + // only need one sample + if (sessionInfoReceived) + return Task.CompletedTask; - // skip 'CarSetup' properties since they are dynamic and can vary widely - missingProperties = allMissingProperties - .Where(prop => !prop.StartsWith("CarSetup")) - .ToList(); + sessionInfoReceived = true; - cts.Cancel(); - break; // exit after first item - } - }); + var allMissingProperties = ValidateModelAgainstYaml(rawYaml); - // Start monitoring - var monitorTask = client.Monitor(cts.Token); + // skip 'CarSetup' properties since they are dynamic and can vary widely + missingProperties = allMissingProperties + .Where(prop => !prop.StartsWith("CarSetup")) + .ToList(); - await Task.WhenAll(rawSessionTask, monitorTask); + cts.Cancel(); + return Task.CompletedTask; + }, + }, + cts.Token); Assert.True(sessionInfoReceived, "Session info was not received within the timeout period."); diff --git a/Sdk/tests/SmokeTests/Base/Base.Variables.cs b/Sdk/tests/SmokeTests/Base/Base.Variables.cs index c348dd5..26d2370 100644 --- a/Sdk/tests/SmokeTests/Base/Base.Variables.cs +++ b/Sdk/tests/SmokeTests/Base/Base.Variables.cs @@ -27,36 +27,36 @@ protected async Task BaseVerifyAllVariablesCovered(ITelemetryClient? allMissingVariables = null; - var telemetryTask = Task.Run(async () => - { - await foreach (var telemetryData in client.TelemetryData) + await client.Monitor( + new TelemetryHandlers { - variablesReceived = true; - - // get all available variable definitions from iRacing - var availableVariables = client.GetTelemetryVariables(); - var availableVariableNames = availableVariables.Select(v => v.Name).ToHashSet(); + OnTelemetryUpdate = _ => + { + // only need one sample + if (variablesReceived) + return Task.CompletedTask; - // get all TelemetryVar enum values - var enumVariables = Enum.GetValues() - .Select(e => e.ToString()) - .ToHashSet(); + variablesReceived = true; - // find variables that exist in iRacing but not in our enum (keep full variable info) - allMissingVariables = availableVariables - .Where(v => !enumVariables.Contains(v.Name)) - .OrderBy(v => v.Name) - .ToList(); + // get all available variable definitions from iRacing + var availableVariables = client.GetTelemetryVariables(); - cts.Cancel(); - break; // exit after first item - } - }); + // get all TelemetryVar enum values + var enumVariables = Enum.GetValues() + .Select(e => e.ToString()) + .ToHashSet(); - // start monitoring - var monitorTask = client.Monitor(cts.Token); + // find variables that exist in iRacing but not in our enum (keep full variable info) + allMissingVariables = availableVariables + .Where(v => !enumVariables.Contains(v.Name)) + .OrderBy(v => v.Name) + .ToList(); - await Task.WhenAll(telemetryTask, monitorTask); + cts.Cancel(); + return Task.CompletedTask; + }, + }, + cts.Token); Assert.True(variablesReceived, "Telemetry data was not received within the timeout period."); diff --git a/Sdk/tests/SmokeTests/SmokeTests.csproj b/Sdk/tests/SmokeTests/SmokeTests.csproj index 69b594d..37e1f71 100644 --- a/Sdk/tests/SmokeTests/SmokeTests.csproj +++ b/Sdk/tests/SmokeTests/SmokeTests.csproj @@ -1,4 +1,4 @@ - + Exe @@ -11,6 +11,7 @@ false true + $(NoWarn);CS0436 @@ -54,8 +55,8 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - - + + diff --git a/Sdk/tests/UnitTests/DataProviders/SessionInfoDecoderTests.cs b/Sdk/tests/UnitTests/DataProviders/SessionInfoDecoderTests.cs new file mode 100644 index 0000000..94d32ce --- /dev/null +++ b/Sdk/tests/UnitTests/DataProviders/SessionInfoDecoderTests.cs @@ -0,0 +1,66 @@ +/** + * Copyright (C) 2024-2026 Scott Velez + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. +**/ + +using System.Collections.Generic; +using System.Text; +using SVappsLAB.iRacingTelemetrySDK.DataProviders; + +namespace UnitTests.DataProviders +{ + public class SessionInfoDecoderTests + { + // "José Muñoz" differs between UTF-8 and Latin1; "René" round-trips through Latin1 + const string MultiByteName = "José Muñoz"; + const string Latin1Name = "René"; + + static byte[] BuildSessionInfo(string? encodingTagValue, string driverName, Encoding payloadEncoding) + { + var sb = new StringBuilder(); + sb.Append("---\n"); + sb.Append("WeekendInfo:\n"); + if (encodingTagValue != null) + sb.Append($" Encoding: {encodingTagValue}\n"); + sb.Append("DriverInfo:\n"); + sb.Append(" Drivers:\n"); + sb.Append($" - UserName: {driverName}\n"); + sb.Append("...\n"); + + return payloadEncoding.GetBytes(sb.ToString()); + } + + [Fact] + public void Utf8Tag_DecodesMultibyteCorrectly() + { + var bytes = BuildSessionInfo("UTF8", MultiByteName, Encoding.UTF8); + + Assert.True(SessionInfoDecoder.IsUtf8(bytes)); + + var result = SessionInfoDecoder.Decode(bytes); + Assert.Contains($"UserName: {MultiByteName}", result); + } + + [Fact] + public void NoTag_DefaultsToIso8859() + { + var bytes = BuildSessionInfo(encodingTagValue: null, Latin1Name, Encoding.Latin1); + + Assert.False(SessionInfoDecoder.IsUtf8(bytes)); + + var result = SessionInfoDecoder.Decode(bytes); + Assert.Contains($"UserName: {Latin1Name}", result); + } + } +} diff --git a/Sdk/tests/UnitTests/TelemetryVarDictionaryTests.cs b/Sdk/tests/UnitTests/TelemetryVarDictionaryTests.cs new file mode 100644 index 0000000..fb68286 --- /dev/null +++ b/Sdk/tests/UnitTests/TelemetryVarDictionaryTests.cs @@ -0,0 +1,57 @@ +/** + * Copyright (C) 2024-2026 Scott Velez + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. +**/ + +using SVappsLAB.iRacingTelemetrySDK; + +namespace UnitTests +{ + // TelemetryVar enum and the iRacingVars dictionary are maintained by hand. + // the compiler guarantees every dictionary entry has an enum key, but not the + // reverse - an enum member without a dictionary entry has no codegen metadata. + // these tests keep the two in sync + public class TelemetryVarDictionaryTests + { + [Fact] + public void EveryEnumValueHasDictionaryEntry() + { + var vars = new iRacingVars().Vars; + + var missing = Enum.GetValues() + .Where(v => !vars.ContainsKey(v)) + .Select(v => v.ToString()) + .ToList(); + + Assert.True(missing.Count == 0, + $"found {missing.Count} TelemetryVar enum values missing from the iRacingVars dictionary: {string.Join(", ", missing)}"); + } + + [Fact] + public void EveryDictionaryEntryNameMatchesItsEnumKey() + { + var vars = new iRacingVars().Vars; + + // the VarItem name string is what's used at runtime to look up the + // variable in the iRacing data, so a typo would silently break the lookup + var mismatched = vars + .Where(kvp => kvp.Key.ToString() != kvp.Value.Name) + .Select(kvp => $"key={kvp.Key}, name=\"{kvp.Value.Name}\"") + .ToList(); + + Assert.True(mismatched.Count == 0, + $"found {mismatched.Count} iRacingVars entries whose Name doesn't match their enum key: {string.Join("; ", mismatched)}"); + } + } +} diff --git a/Sdk/tests/UnitTests/UnitTests.csproj b/Sdk/tests/UnitTests/UnitTests.csproj index 9207687..8343414 100644 --- a/Sdk/tests/UnitTests/UnitTests.csproj +++ b/Sdk/tests/UnitTests/UnitTests.csproj @@ -30,7 +30,7 @@ - + @@ -42,6 +42,9 @@ + + +