Skip to content

Commit aeabfe5

Browse files
committed
ci(audience): add dotnet test project and GitHub Actions workflow (SDK-128)
- Audience.Runtime.csproj + Audience.Tests.csproj: dotnet test harness for pure-C# modules (no Unity license needed in CI) - AssemblyInfo.cs: InternalsVisibleTo for test assembly, fixes internal visibility for both Unity test runner and dotnet - test-audience-sdk.yml: workflow triggers on PRs/pushes touching src/Packages/Audience/** - Fix Identity.Reset() to swallow DirectoryNotFoundException (not just FileNotFoundException) when consent=None left no directory on disk
1 parent 61bec02 commit aeabfe5

7 files changed

Lines changed: 153 additions & 34 deletions

File tree

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
name: Audience package — Unit Tests
2+
3+
on:
4+
push:
5+
branches: [main]
6+
paths:
7+
- 'src/Packages/Audience/**'
8+
- '.github/workflows/test-audience-sdk.yml'
9+
pull_request:
10+
paths:
11+
- 'src/Packages/Audience/**'
12+
- '.github/workflows/test-audience-sdk.yml'
13+
14+
jobs:
15+
test:
16+
name: Unit Tests (.NET)
17+
runs-on: ubuntu-latest
18+
19+
steps:
20+
- uses: actions/checkout@v4
21+
22+
- name: Setup .NET
23+
uses: actions/setup-dotnet@v4
24+
with:
25+
dotnet-version: '8.0.x'
26+
27+
- name: Restore dependencies
28+
run: dotnet restore src/Packages/Audience/Tests/Audience.Tests.csproj
29+
30+
- name: Build
31+
run: dotnet build src/Packages/Audience/Tests/Audience.Tests.csproj --no-restore --configuration Release
32+
33+
- name: Run tests
34+
run: >
35+
dotnet test src/Packages/Audience/Tests/Audience.Tests.csproj
36+
--no-build --configuration Release
37+
--logger "trx;LogFileName=test-results.trx"
38+
--logger "console;verbosity=normal"
39+
40+
- name: Upload test results
41+
uses: actions/upload-artifact@v4
42+
if: always()
43+
with:
44+
name: audience-test-results
45+
path: '**/test-results.trx'
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
using System.Runtime.CompilerServices;
2+
3+
[assembly: InternalsVisibleTo("Immutable.Audience.Runtime.Tests")]
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
<PropertyGroup>
3+
<TargetFramework>netstandard2.1</TargetFramework>
4+
<LangVersion>9.0</LangVersion>
5+
<Nullable>disable</Nullable>
6+
<!-- Match the Unity asmdef assembly name so InternalsVisibleTo works in both contexts -->
7+
<AssemblyName>Immutable.Audience.Runtime</AssemblyName>
8+
</PropertyGroup>
9+
<!--
10+
Exclude modules that depend on Unity APIs (added in later PRs).
11+
These compile fine in Unity but cannot be built with the .NET SDK alone.
12+
Update this list as Unity-specific modules are added under Collection/ and Utility/MainThreadDispatcher.cs.
13+
-->
14+
</Project>

src/Packages/Audience/Runtime/Core/Identity.cs

Lines changed: 29 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ namespace Immutable.Audience
66
internal sealed class Identity
77
{
88
private static volatile string _cachedId;
9+
private static readonly object _sync = new object();
910

1011
private static string GetDirectory(string persistentDataPath) =>
1112
Path.Combine(persistentDataPath, "imtbl_audience");
@@ -21,35 +22,38 @@ internal static string GetOrCreate(string persistentDataPath, ConsentLevel conse
2122
if (_cachedId != null)
2223
return _cachedId;
2324

24-
var dir = GetDirectory(persistentDataPath);
25-
Directory.CreateDirectory(dir);
25+
lock (_sync)
26+
{
27+
if (_cachedId != null)
28+
return _cachedId;
2629

27-
var filePath = GetFilePath(persistentDataPath);
30+
var dir = GetDirectory(persistentDataPath);
31+
Directory.CreateDirectory(dir);
2832

29-
if (File.Exists(filePath))
30-
{
31-
var existing = File.ReadAllText(filePath).Trim();
32-
_cachedId = existing;
33-
return _cachedId;
34-
}
33+
var filePath = GetFilePath(persistentDataPath);
3534

36-
var newId = Guid.NewGuid().ToString();
37-
var tmpPath = filePath + ".tmp";
38-
File.WriteAllText(tmpPath, newId);
35+
if (File.Exists(filePath))
36+
{
37+
_cachedId = File.ReadAllText(filePath).Trim();
38+
return _cachedId;
39+
}
3940

40-
try
41-
{
42-
File.Move(tmpPath, filePath);
43-
}
44-
catch (IOException)
45-
{
46-
// Destination already exists (race condition) — delete it and retry
47-
File.Delete(filePath);
48-
File.Move(tmpPath, filePath);
49-
}
41+
var newId = Guid.NewGuid().ToString();
42+
var tmpPath = filePath + ".tmp";
43+
File.WriteAllText(tmpPath, newId);
44+
try
45+
{
46+
File.Move(tmpPath, filePath);
47+
}
48+
catch (IOException)
49+
{
50+
File.Delete(filePath);
51+
File.Move(tmpPath, filePath);
52+
}
5053

51-
_cachedId = newId;
52-
return _cachedId;
54+
_cachedId = newId;
55+
return _cachedId;
56+
}
5357
}
5458

5559
internal static void Reset(string persistentDataPath)
@@ -61,7 +65,7 @@ internal static void Reset(string persistentDataPath)
6165
{
6266
File.Delete(filePath);
6367
}
64-
catch (FileNotFoundException)
68+
catch (Exception e) when (e is FileNotFoundException || e is DirectoryNotFoundException)
6569
{
6670
// Nothing to delete — this is fine
6771
}

src/Packages/Audience/Runtime/Utility/Json.cs

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -38,17 +38,11 @@ private static void WriteValue(StringBuilder sb, object value)
3838
}
3939
else if (value is float f)
4040
{
41-
var result = f.ToString("G", CultureInfo.InvariantCulture);
42-
if (result.IndexOf('E') >= 0 || result.IndexOf('e') >= 0)
43-
result = f.ToString("F6", CultureInfo.InvariantCulture);
44-
sb.Append(result);
41+
sb.Append(f.ToString("R", CultureInfo.InvariantCulture));
4542
}
4643
else if (value is double d)
4744
{
48-
var result = d.ToString("G", CultureInfo.InvariantCulture);
49-
if (result.IndexOf('E') >= 0 || result.IndexOf('e') >= 0)
50-
result = d.ToString("F6", CultureInfo.InvariantCulture);
51-
sb.Append(result);
45+
sb.Append(d.ToString("R", CultureInfo.InvariantCulture));
5246
}
5347
else if (value is decimal dec)
5448
{
@@ -120,7 +114,7 @@ private static void WriteString(StringBuilder sb, string s)
120114
break;
121115
default:
122116
if (c < 0x20)
123-
sb.AppendFormat("\\u{0:X4}", (int)c);
117+
sb.Append("\\u").Append(((int)c).ToString("X4"));
124118
else
125119
sb.Append(c);
126120
break;
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
<PropertyGroup>
3+
<TargetFramework>net8.0</TargetFramework>
4+
<LangVersion>9.0</LangVersion>
5+
<Nullable>disable</Nullable>
6+
<IsPackable>false</IsPackable>
7+
<!-- Match the Unity asmdef assembly name so InternalsVisibleTo("Immutable.Audience.Runtime.Tests") resolves correctly -->
8+
<AssemblyName>Immutable.Audience.Runtime.Tests</AssemblyName>
9+
</PropertyGroup>
10+
<ItemGroup>
11+
<PackageReference Include="NUnit" Version="3.14.0" />
12+
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
13+
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.9.0" />
14+
</ItemGroup>
15+
<ItemGroup>
16+
<ProjectReference Include="../Runtime/Audience.Runtime.csproj" />
17+
</ItemGroup>
18+
</Project>

src/Packages/Audience/Tests/Runtime/JsonTests.cs

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,47 @@ public void Serialize_NestedDict_ReturnsNestedObject()
9393
Assert.AreEqual("{\"outer\":{\"inner\":\"value\"}}", Json.Serialize(data));
9494
}
9595

96+
[Test]
97+
public void Serialize_FloatValue_NormalRange()
98+
{
99+
var data = new Dictionary<string, object> { { "v", 3.14f } };
100+
var result = Json.Serialize(data);
101+
StringAssert.Contains("\"v\":", result);
102+
StringAssert.DoesNotContain("\"v\":\"", result); // must not be quoted
103+
}
104+
105+
[Test]
106+
public void Serialize_FloatValue_LargeExponent_PreservesValue()
107+
{
108+
// 1e30f in scientific notation is valid JSON — must not be silently zeroed
109+
var data = new Dictionary<string, object> { { "v", 1e30f } };
110+
var result = Json.Serialize(data);
111+
var serialised = result.Substring(result.IndexOf(':') + 1, result.Length - result.IndexOf(':') - 2);
112+
Assert.AreNotEqual("0", serialised);
113+
Assert.AreNotEqual("0.000000", serialised);
114+
}
115+
116+
[Test]
117+
public void Serialize_FloatValue_SmallNegativeExponent_PreservesValue()
118+
{
119+
// 1e-30f — the old F6 fallback turned this into "0.000000"
120+
var data = new Dictionary<string, object> { { "v", 1e-30f } };
121+
var result = Json.Serialize(data);
122+
var serialised = result.Substring(result.IndexOf(':') + 1, result.Length - result.IndexOf(':') - 2);
123+
Assert.AreNotEqual("0", serialised);
124+
Assert.AreNotEqual("0.000000", serialised);
125+
}
126+
127+
[Test]
128+
public void Serialize_DoubleValue_SmallNegativeExponent_PreservesValue()
129+
{
130+
var data = new Dictionary<string, object> { { "v", 1e-300 } };
131+
var result = Json.Serialize(data);
132+
var serialised = result.Substring(result.IndexOf(':') + 1, result.Length - result.IndexOf(':') - 2);
133+
Assert.AreNotEqual("0", serialised);
134+
Assert.AreNotEqual("0.000000", serialised);
135+
}
136+
96137
[Test]
97138
public void Serialize_ListValue_ReturnsJsonArray()
98139
{

0 commit comments

Comments
 (0)