Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,7 @@ jobs:
run: dotnet build src/Oddin.OddsFeedSdk/Oddin.OddsFeedSdk/Oddin.OddsFeedSdk.csproj --configuration Release --no-restore

- name: Run Library Tests
# This step will pass if no tests are found, but will run them once added.
run: dotnet test src/Oddin.OddsFeedSdk/Oddin.OddsFeedSdk/Oddin.OddsFeedSdk.csproj --no-build
run: dotnet test src/Oddin.OddsFeedSdk/Oddin.OddsFeedSdk.Tests/Oddin.OddsFeedSdk.Tests.csproj

- name: Extract version from tag
id: version
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<LangVersion>10</LangVersion>
<IsPackable>false</IsPackable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.2"/>
<PackageReference Include="xunit" Version="2.4.2"/>
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>

<ItemGroup>
<ProjectReference Include="../Oddin.OddsFeedSdk/Oddin.OddsFeedSdk.csproj"/>
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
using System.IO;
using System.Xml.Serialization;
using Oddin.OddsFeedSdk.AMQP.Messages;
using Oddin.OddsFeedSdk.API.Models;
using Xunit;

namespace Oddin.OddsFeedSdk.Tests;

// Set-based classic sports report the games of each set on the period row,
// next to the running sets-won tally (home_score/away_score). Before games
// were modelled on period_score, XmlSerializer silently dropped the attribute
// and a consumer rendering per-set rows showed the tally (1:0) where the set
// was actually 6:4. The scoreboard cannot recover it either - its games reset
// with every new set. The Specified flags drive the null mapping in
// MatchStatusCache, keeping absence distinguishable from a real 0:0.
public class PeriodScoreGamesTests
{
[Fact]
public void FeedPeriodScoresCarryGames()
{
const string raw = @"<odds_change event_id=""od:match:11"" product=""1"" timestamp=""1785831336922"">
<sport_event_status status=""1"" match_status=""201"" home_score=""1"" away_score=""0"">
<period_scores>
<period_score type=""set"" number=""1"" match_status_code=""200"" home_score=""1"" away_score=""0"" home_games=""6"" away_games=""4""/>
<period_score type=""set"" number=""2"" match_status_code=""201"" home_score=""1"" away_score=""0"" home_games=""0"" away_games=""1""/>
<period_score type=""map"" number=""3"" match_status_code=""100"" home_score=""1"" away_score=""0"" home_won_rounds=""13""/>
</period_scores>
</sport_event_status>
<odds/>
</odds_change>";

var serializer = new XmlSerializer(typeof(odds_change));
using var reader = new StringReader(raw);
var message = (odds_change)serializer.Deserialize(reader);

var periodScores = message.sport_event_status.period_scores.period_score;
Assert.Equal(3, periodScores.Length);

// A completed set keeps its games; the set in progress carries current games.
Assert.True(periodScores[0].home_gamesSpecified);
Assert.True(periodScores[0].away_gamesSpecified);
Assert.Equal(6, periodScores[0].home_games);
Assert.Equal(4, periodScores[0].away_games);
Assert.True(periodScores[1].home_gamesSpecified);
Assert.True(periodScores[1].away_gamesSpecified);
Assert.Equal(0, periodScores[1].home_games);
Assert.Equal(1, periodScores[1].away_games);

// A period without games leaves the Specified flags unset, so the cache
// maps them to null instead of a false 0:0.
Assert.False(periodScores[2].home_gamesSpecified);
Assert.False(periodScores[2].away_gamesSpecified);

// The sets-won tally on the same row must stay readable and independent.
Assert.Equal(1, periodScores[0].home_score);
Assert.Equal(0, periodScores[0].away_score);
}

[Fact]
public void ApiPeriodScoresCarryGames()
{
const string raw = @"<?xml version=""1.0""?>
<match_summary generated_at=""2026-01-01T00:00:00Z"">
<sport_event id=""od:match:9"" scheduled=""2026-01-01T12:00:00Z""/>
<sport_event_status status=""live"" match_status_code=""201"" home_score=""1"" away_score=""0"">
<period_scores>
<period_score type=""set"" number=""1"" match_status_code=""200"" home_score=""1"" away_score=""0"" home_games=""6"" away_games=""4""/>
<period_score type=""set"" number=""2"" match_status_code=""201"" home_score=""1"" away_score=""0""/>
</period_scores>
</sport_event_status>
</match_summary>";

var serializer = new XmlSerializer(typeof(MatchSummaryModel));
using var reader = new StringReader(raw);
var summary = (MatchSummaryModel)serializer.Deserialize(reader);

var periodScores = summary.sport_event_status.period_scores;
Assert.Equal(2, periodScores.Length);

Assert.True(periodScores[0].home_gamesSpecified);
Assert.True(periodScores[0].away_gamesSpecified);
Assert.Equal(6, periodScores[0].home_games);
Assert.Equal(4, periodScores[0].away_games);

Assert.False(periodScores[1].home_gamesSpecified);
Assert.False(periodScores[1].away_gamesSpecified);
}
}
34 changes: 34 additions & 0 deletions src/Oddin.OddsFeedSdk/Oddin.OddsFeedSdk.sln
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,54 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Oddin.OddsFeedSdk", "Oddin.
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Oddin.OddsFeedSdkDemoIntegration", "Oddin.OddsFeedSdkDemoIntegration\Oddin.OddsFeedSdkDemoIntegration.csproj", "{D82F279F-1979-4EE8-A47B-7B75739E22E5}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Oddin.OddsFeedSdk.Tests", "Oddin.OddsFeedSdk.Tests\Oddin.OddsFeedSdk.Tests.csproj", "{DC32845D-C3E6-4D65-A0D3-ACFF6A7AFDFD}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Debug|x64 = Debug|x64
Debug|x86 = Debug|x86
Release|Any CPU = Release|Any CPU
Release|x64 = Release|x64
Release|x86 = Release|x86
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{8FD0F8D1-773B-4095-A455-BC5B3C45562A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8FD0F8D1-773B-4095-A455-BC5B3C45562A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8FD0F8D1-773B-4095-A455-BC5B3C45562A}.Debug|x64.ActiveCfg = Debug|Any CPU
{8FD0F8D1-773B-4095-A455-BC5B3C45562A}.Debug|x64.Build.0 = Debug|Any CPU
{8FD0F8D1-773B-4095-A455-BC5B3C45562A}.Debug|x86.ActiveCfg = Debug|Any CPU
{8FD0F8D1-773B-4095-A455-BC5B3C45562A}.Debug|x86.Build.0 = Debug|Any CPU
{8FD0F8D1-773B-4095-A455-BC5B3C45562A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8FD0F8D1-773B-4095-A455-BC5B3C45562A}.Release|Any CPU.Build.0 = Release|Any CPU
{8FD0F8D1-773B-4095-A455-BC5B3C45562A}.Release|x64.ActiveCfg = Release|Any CPU
{8FD0F8D1-773B-4095-A455-BC5B3C45562A}.Release|x64.Build.0 = Release|Any CPU
{8FD0F8D1-773B-4095-A455-BC5B3C45562A}.Release|x86.ActiveCfg = Release|Any CPU
{8FD0F8D1-773B-4095-A455-BC5B3C45562A}.Release|x86.Build.0 = Release|Any CPU
{D82F279F-1979-4EE8-A47B-7B75739E22E5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{D82F279F-1979-4EE8-A47B-7B75739E22E5}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D82F279F-1979-4EE8-A47B-7B75739E22E5}.Debug|x64.ActiveCfg = Debug|Any CPU
{D82F279F-1979-4EE8-A47B-7B75739E22E5}.Debug|x64.Build.0 = Debug|Any CPU
{D82F279F-1979-4EE8-A47B-7B75739E22E5}.Debug|x86.ActiveCfg = Debug|Any CPU
{D82F279F-1979-4EE8-A47B-7B75739E22E5}.Debug|x86.Build.0 = Debug|Any CPU
{D82F279F-1979-4EE8-A47B-7B75739E22E5}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D82F279F-1979-4EE8-A47B-7B75739E22E5}.Release|Any CPU.Build.0 = Release|Any CPU
{D82F279F-1979-4EE8-A47B-7B75739E22E5}.Release|x64.ActiveCfg = Release|Any CPU
{D82F279F-1979-4EE8-A47B-7B75739E22E5}.Release|x64.Build.0 = Release|Any CPU
{D82F279F-1979-4EE8-A47B-7B75739E22E5}.Release|x86.ActiveCfg = Release|Any CPU
{D82F279F-1979-4EE8-A47B-7B75739E22E5}.Release|x86.Build.0 = Release|Any CPU
{DC32845D-C3E6-4D65-A0D3-ACFF6A7AFDFD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{DC32845D-C3E6-4D65-A0D3-ACFF6A7AFDFD}.Debug|Any CPU.Build.0 = Debug|Any CPU
{DC32845D-C3E6-4D65-A0D3-ACFF6A7AFDFD}.Debug|x64.ActiveCfg = Debug|Any CPU
{DC32845D-C3E6-4D65-A0D3-ACFF6A7AFDFD}.Debug|x64.Build.0 = Debug|Any CPU
{DC32845D-C3E6-4D65-A0D3-ACFF6A7AFDFD}.Debug|x86.ActiveCfg = Debug|Any CPU
{DC32845D-C3E6-4D65-A0D3-ACFF6A7AFDFD}.Debug|x86.Build.0 = Debug|Any CPU
{DC32845D-C3E6-4D65-A0D3-ACFF6A7AFDFD}.Release|Any CPU.ActiveCfg = Release|Any CPU
{DC32845D-C3E6-4D65-A0D3-ACFF6A7AFDFD}.Release|Any CPU.Build.0 = Release|Any CPU
{DC32845D-C3E6-4D65-A0D3-ACFF6A7AFDFD}.Release|x64.ActiveCfg = Release|Any CPU
{DC32845D-C3E6-4D65-A0D3-ACFF6A7AFDFD}.Release|x64.Build.0 = Release|Any CPU
{DC32845D-C3E6-4D65-A0D3-ACFF6A7AFDFD}.Release|x86.ActiveCfg = Release|Any CPU
{DC32845D-C3E6-4D65-A0D3-ACFF6A7AFDFD}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1368,6 +1368,22 @@ public double away_score
[System.Xml.Serialization.XmlAttributeAttribute()]
public int away_points { get; set; }

// Games won inside one period. Set-based classic sports (tennis) report
// them per set, alongside home_score/away_score, which is the running
// sets-won tally. The Specified pattern keeps absence distinguishable
// from a real 0:0 - XmlSerializer cannot handle nullable attributes.
[System.Xml.Serialization.XmlAttributeAttribute()]
public int home_games { get; set; }

[System.Xml.Serialization.XmlIgnoreAttribute()]
public bool home_gamesSpecified { get; set; }

[System.Xml.Serialization.XmlAttributeAttribute()]
public int away_games { get; set; }

[System.Xml.Serialization.XmlIgnoreAttribute()]
public bool away_gamesSpecified { get; set; }

[System.Xml.Serialization.XmlAttributeAttribute()]
public int home_kills { get; set; }

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ public interface IPeriodScore
int? AwayGoals { get; }
int? HomePoints { get; }
int? AwayPoints { get; }
int? HomeGames { get; }
int? AwayGames { get; }
int? HomeRuns { get; }
int? AwayRuns { get; }
int? HomeWicketsFallen { get; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ public PeriodScore(
int? awayGoals,
int? homePoints,
int? awayPoints,
int? homeGames,
int? awayGames,
int? homeRuns,
int? awayRuns,
int? homeWicketsFallen,
Expand All @@ -42,6 +44,8 @@ public PeriodScore(
AwayGoals = awayGoals;
HomePoints = homePoints;
AwayPoints = awayPoints;
HomeGames = homeGames;
AwayGames = awayGames;
HomeRuns = homeRuns;
AwayRuns = awayRuns;
HomeWicketsFallen = homeWicketsFallen;
Expand All @@ -66,6 +70,8 @@ public PeriodScore(
public int? AwayGoals { get; }
public int? HomePoints { get; }
public int? AwayPoints { get; }
public int? HomeGames { get; }
public int? AwayGames { get; }
public int? HomeRuns { get; }
public int? AwayRuns { get; }
public int? HomeWicketsFallen { get; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,8 @@ private List<PeriodScore> MapApiPeriodScores(periodScore[] scores)
s.away_goals,
s.home_points,
s.away_points,
s.home_gamesSpecified ? s.home_games : null,
s.away_gamesSpecified ? s.away_games : null,
s.home_runs,
s.away_runs,
s.home_wickets_fallen,
Expand Down Expand Up @@ -294,6 +296,8 @@ private List<PeriodScore> MapFeedPeriodScores(periodScoreType[] scores)
s.away_goals,
s.home_points,
s.away_points,
s.home_gamesSpecified ? s.home_games : null,
s.away_gamesSpecified ? s.away_games : null,
s.home_runs,
s.away_runs,
s.home_wickets_fallen,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -491,6 +491,22 @@ public int away_goals
[System.Xml.Serialization.XmlAttributeAttribute()]
public int away_points { get; set; }

// Games won inside one period. Set-based classic sports (tennis) report
// them per set, alongside home_score/away_score, which is the running
// sets-won tally. The Specified pattern keeps absence distinguishable
// from a real 0:0 - XmlSerializer cannot handle nullable attributes.
[System.Xml.Serialization.XmlAttributeAttribute()]
public int home_games { get; set; }

[System.Xml.Serialization.XmlIgnoreAttribute()]
public bool home_gamesSpecified { get; set; }

[System.Xml.Serialization.XmlAttributeAttribute()]
public int away_games { get; set; }

[System.Xml.Serialization.XmlIgnoreAttribute()]
public bool away_gamesSpecified { get; set; }

[System.Xml.Serialization.XmlAttributeAttribute()]
public int home_kills { get; set; }

Expand Down
Loading