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
12 changes: 12 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,18 @@ Run tests with verbose output:
bin/test --verbose
```

### Code Coverage

```bash
dotnet test tests/PostHog.Unity.Tests --settings tests/coverage.runsettings \
--collect:"XPlat Code Coverage" --results-directory TestResults
```

The runtime sources are compiled directly into the test assembly, so the coverage
settings include that assembly while excluding test code and the generated test
entry point. Compare runs with the same settings and build configuration.
Coverage measures the .NET/Unity-stub build, not native Unity or WebGL execution.

### Formatting Code

Format all C# files:
Expand Down
26 changes: 17 additions & 9 deletions tests/PostHog.Unity.Tests/FeatureFlagModelTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,9 @@ public void WithMetadata_IncludesMetadata()

var dict = flag.ToDictionary();

Assert.True(dict.ContainsKey("metadata"));
var metadata = Assert.IsType<Dictionary<string, object>>(dict["metadata"]);
Assert.Equal(1, metadata["id"]);
Assert.Equal(2, metadata["version"]);
}

[Fact]
Expand All @@ -153,7 +155,8 @@ public void WithReason_IncludesReason()

var dict = flag.ToDictionary();

Assert.True(dict.ContainsKey("reason"));
var reason = Assert.IsType<Dictionary<string, object>>(dict["reason"]);
Assert.Equal("test", reason["description"]);
}

[Fact]
Expand Down Expand Up @@ -255,7 +258,8 @@ public void WithPayload_IncludesPayload()

var dict = metadata.ToDictionary();

Assert.True(dict.ContainsKey("payload"));
var payload = Assert.IsType<Dictionary<string, object>>(dict["payload"]);
Assert.Equal("value", payload["key"]);
}

[Fact]
Expand Down Expand Up @@ -397,8 +401,7 @@ public void WithQuotaLimited_ParsesList()
var result = FeatureFlagsResponse.FromDictionary(dict);

Assert.NotNull(result.QuotaLimited);
Assert.Equal(2, result.QuotaLimited.Count);
Assert.Contains("flag1", result.QuotaLimited);
Assert.Equal(new[] { "flag1", "flag2" }, result.QuotaLimited);
}

[Fact]
Expand Down Expand Up @@ -454,7 +457,8 @@ public void WithFeatureFlags_IncludesFlags()

var dict = response.ToDictionary();

Assert.True(dict.ContainsKey("featureFlags"));
var flags = Assert.IsType<Dictionary<string, object>>(dict["featureFlags"]);
Assert.Equal(true, flags["flag"]);
}

[Fact]
Expand All @@ -470,9 +474,9 @@ public void WithV4Flags_SerializesFlags()

var dict = response.ToDictionary();

Assert.True(dict.ContainsKey("flags"));
var flags = (Dictionary<string, object>)dict["flags"];
Assert.True(flags.ContainsKey("test"));
var flags = Assert.IsType<Dictionary<string, object>>(dict["flags"]);
var flag = Assert.IsType<Dictionary<string, object>>(flags["test"]);
Assert.Equal(true, flag["enabled"]);
}

[Fact]
Expand Down Expand Up @@ -529,6 +533,10 @@ public void FromDictionaryAndToDictionary_PreservesData()
var response = FeatureFlagsResponse.FromDictionary(original);
var result = response.ToDictionary();

var flags = Assert.IsType<Dictionary<string, object>>(result["featureFlags"]);
Assert.Equal(2, flags.Count);
Assert.Equal(true, flags["flag1"]);
Assert.Equal("variant", flags["flag2"]);
Assert.Equal(false, result["errorsWhileComputingFlags"]);
Assert.Equal("test-request", result["requestId"]);
Assert.Equal(1700000000L, result["evaluatedAt"]);
Expand Down
30 changes: 25 additions & 5 deletions tests/PostHog.Unity.Tests/FileStorageProviderTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ public FileStorageProviderTests()

public void Dispose()
{
_storage.FlushPendingWrites();
try
{
if (Directory.Exists(_testBasePath))
Expand Down Expand Up @@ -206,7 +207,9 @@ public void WaitsForPendingWrite_BeforeDeleting()

// Assert - file should be deleted
var filePath = Path.Combine(_testBasePath, "queue", $"{eventId}.json");
_storage.FlushPendingWrites();
Assert.False(File.Exists(filePath));
Assert.DoesNotContain(eventId, _storage.GetEventIds());
}

[Fact]
Expand Down Expand Up @@ -300,8 +303,9 @@ public void WaitsForPendingWrites_BeforeClearing()
// Act - clear immediately after save
_storage.Clear();

// Assert - index should be empty
_storage.FlushPendingWrites();
Assert.Empty(_storage.GetEventIds());
Assert.Empty(Directory.GetFiles(Path.Combine(_testBasePath, "queue"), "*.json"));
}

[Fact]
Expand Down Expand Up @@ -417,7 +421,14 @@ public async Task ConcurrentSaves_DoNotCorruptIndex()

// Assert
var ids = _storage.GetEventIds();
Assert.Equal(eventCount, ids.Count);
Assert.Equal(
Enumerable
.Range(0, eventCount)
.Select(i => $"concurrent-{i}")
.OrderBy(id => id),
ids.OrderBy(id => id)
);
Assert.All(ids, id => Assert.Equal("{}", _storage.LoadEvent(id)));
}

[Fact]
Expand Down Expand Up @@ -475,9 +486,18 @@ public async Task ConcurrentSavesAndDeletes_DoNotThrow()
await Task.WhenAll(tasks);
_storage.FlushPendingWrites();

// Roughly half should remain (the odd-numbered ones)
var remaining = _storage.GetEventIds();
Assert.True(remaining.Count >= eventCount / 2 - 5); // Allow some tolerance
var expected = Enumerable
.Range(0, eventCount)
.Where(i => i % 2 != 0)
.Select(i => $"save-delete-{i}")
.OrderBy(id => id)
.ToArray();
Assert.Equal(expected, _storage.GetEventIds().OrderBy(id => id));
var persistedIds = Directory
.GetFiles(Path.Combine(_testBasePath, "queue"), "*.json")
.Select(Path.GetFileNameWithoutExtension)
.OrderBy(id => id);
Assert.Equal(expected, persistedIds);
}
}
}
Expand Down
63 changes: 56 additions & 7 deletions tests/PostHog.Unity.Tests/FlagCacheTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -162,9 +162,46 @@ public void WithNull_DoesNothing()
Assert.False(cache.IsLoaded);
}

// Note: QuotaLimited test removed because it triggers Unity Debug.LogWarning
// which isn't available in the xUnit test environment. The quota limited
// behavior is tested via integration tests in Unity.
[Fact]
public void QuotaLimited_ClearsFlagsAndPayloadsFromMemoryAndDisk()
{
var storage = new InMemoryStorageProvider();
var cache = new FlagCache(storage);
cache.Update(
new FeatureFlagsResponse
{
FeatureFlags = new Dictionary<string, object> { ["v3"] = true },
FeatureFlagPayloads = new Dictionary<string, object> { ["v3"] = "payload" },
Flags = new Dictionary<string, FeatureFlag>
{
["v4"] = new FeatureFlag
{
Enabled = true,
Metadata = new FeatureFlagMetadata { Payload = "v4-payload" },
},
},
}
);
Assert.Equal(true, cache.GetFlag("v3"));
Assert.Equal(true, cache.GetFlag("v4"));

cache.Update(
new FeatureFlagsResponse { QuotaLimited = new List<string> { "feature_flags" } }
);
var reloaded = new FlagCache(storage);
reloaded.LoadFromDisk();

foreach (var actual in new[] { cache, reloaded })
{
Assert.True(actual.IsLoaded);
Assert.Empty(actual.GetAllFlagKeys());
Assert.Null(actual.GetFlag("v3"));
Assert.Null(actual.GetFlag("v4"));
Assert.Null(actual.GetPayload("v3"));
Assert.Null(actual.GetPayload("v4"));
Assert.Null(actual.GetFlagDetails("v4"));
}
}

[Fact]
public void SavesToDisk()
Expand All @@ -180,7 +217,14 @@ public void SavesToDisk()

var savedData = storage.LoadState("feature_flags");
Assert.NotNull(savedData);
Assert.Contains("flag", savedData);
using var saved = System.Text.Json.JsonDocument.Parse(savedData);
Assert.True(
saved.RootElement.GetProperty("featureFlags").GetProperty("flag").GetBoolean()
);
var reloaded = new FlagCache(storage);
reloaded.LoadFromDisk();
Assert.True(reloaded.IsLoaded);
Assert.Equal(true, reloaded.GetFlag("flag"));
}

[Fact]
Expand Down Expand Up @@ -374,8 +418,8 @@ public void WithV4Payload_ReturnsPayload()
}
);

var payload = cache.GetPayload("flag");
Assert.NotNull(payload);
var payload = Assert.IsType<Dictionary<string, object>>(cache.GetPayload("flag"));
Assert.Equal("value", payload["key"]);
}

[Fact]
Expand Down Expand Up @@ -566,7 +610,8 @@ public void ConcurrentReadsAndWrites_DoNotCorruptData()
{
cache.GetFlag("flag-1-1");
cache.GetPayload("flag-1-1");
cache.GetAllFlagKeys();
var keys = cache.GetAllFlagKeys();
Assert.InRange(keys.Count, 0, 1);
}
})
);
Expand All @@ -575,6 +620,10 @@ public void ConcurrentReadsAndWrites_DoNotCorruptData()
var ex = Record.Exception(() => Task.WaitAll(tasks.ToArray()));

Assert.Null(ex);
Assert.True(cache.IsLoaded);
var finalKey = Assert.Single(cache.GetAllFlagKeys());
Assert.Matches(@"^flag-[0-4]-99$", finalKey);
Assert.Equal(true, cache.GetFlag(finalKey));
}
}
}
Expand Down
9 changes: 8 additions & 1 deletion tests/PostHog.Unity.Tests/FlagCalledTrackerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@ public void AllowsRetrackingAfterReset()
public class ThreadSafety
{
[Fact]
public async Task ConcurrentAccess_DoesNotCorruptState()
public async Task ConcurrentUniqueKeys_AreAllTrackedAndRemembered()
{
var tracker = new FlagCalledTracker(1000);
var tasks = new List<Task>();
Expand All @@ -207,6 +207,13 @@ public async Task ConcurrentAccess_DoesNotCorruptState()

// Each unique combination should be tracked exactly once
Assert.Equal(1000, trackedCount);
for (var threadId = 0; threadId < 10; threadId++)
{
for (var j = 0; j < 100; j++)
{
Assert.False(tracker.ShouldTrack($"user-{threadId}-{j}", "flag", true));
}
}
}
}
}
Expand Down
47 changes: 28 additions & 19 deletions tests/PostHog.Unity.Tests/JsonSerializerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -103,11 +103,11 @@ public void WithDictionaryContainingMultipleValues_ReturnsObject()
};
var result = JsonSerializer.Serialize(dict);

Assert.StartsWith("{", result);
Assert.EndsWith("}", result);
Assert.Contains("\"name\":\"test\"", result);
Assert.Contains("\"count\":42", result);
Assert.Contains("\"active\":true", result);
using var document = System.Text.Json.JsonDocument.Parse(result);
var root = document.RootElement;
Assert.Equal("test", root.GetProperty("name").GetString());
Assert.Equal(42, root.GetProperty("count").GetInt32());
Assert.True(root.GetProperty("active").GetBoolean());
}

[Fact]
Expand Down Expand Up @@ -180,12 +180,14 @@ public void WithBasicEvent_ReturnsValidJson()

var result = JsonSerializer.SerializeEvent(evt);

Assert.Contains("\"event\":\"test_event\"", result);
Assert.Contains("\"distinct_id\":\"user123\"", result);
Assert.Contains("\"uuid\":", result);
Assert.Contains($"\"timestamp\":\"{evt.Timestamp}\"", result);
using var document = System.Text.Json.JsonDocument.Parse(result);
var root = document.RootElement;
Assert.Equal("test_event", root.GetProperty("event").GetString());
Assert.Equal("user123", root.GetProperty("distinct_id").GetString());
Assert.Equal(evt.Uuid, root.GetProperty("uuid").GetString());
Assert.Equal(evt.Timestamp, root.GetProperty("timestamp").GetString());
Assert.Matches(@"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{7}Z$", evt.Timestamp);
Assert.Contains("\"properties\":{}", result);
Assert.Empty(root.GetProperty("properties").EnumerateObject());
}

[Fact]
Expand All @@ -200,8 +202,10 @@ public void WithProperties_IncludesProperties()

var result = JsonSerializer.SerializeEvent(evt);

Assert.Contains("\"$lib\":\"posthog-unity\"", result);
Assert.Contains("\"custom\":\"value\"", result);
using var document = System.Text.Json.JsonDocument.Parse(result);
var properties = document.RootElement.GetProperty("properties");
Assert.Equal("posthog-unity", properties.GetProperty("$lib").GetString());
Assert.Equal("value", properties.GetProperty("custom").GetString());
}
}

Expand All @@ -214,10 +218,12 @@ public void WithEmptyBatch_ReturnsValidJson()

var result = JsonSerializer.SerializeBatch(payload);

Assert.Contains("\"api_key\":\"test_api_key\"", result);
Assert.Contains($"\"sent_at\":\"{payload.SentAt}\"", result);
using var document = System.Text.Json.JsonDocument.Parse(result);
var root = document.RootElement;
Assert.Equal("test_api_key", root.GetProperty("api_key").GetString());
Assert.Equal(payload.SentAt, root.GetProperty("sent_at").GetString());
Assert.Matches(@"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{7}Z$", payload.SentAt);
Assert.Contains("\"batch\":[]", result);
Assert.Empty(root.GetProperty("batch").EnumerateArray());
}

[Fact]
Expand All @@ -232,10 +238,13 @@ public void WithEvents_IncludesEvents()

var result = JsonSerializer.SerializeBatch(payload);

Assert.Contains("\"event\":\"event1\"", result);
Assert.Contains("\"event\":\"event2\"", result);
Assert.Contains("\"distinct_id\":\"user1\"", result);
Assert.Contains("\"distinct_id\":\"user2\"", result);
using var document = System.Text.Json.JsonDocument.Parse(result);
var batch = document.RootElement.GetProperty("batch");
Assert.Equal(2, batch.GetArrayLength());
Assert.Equal("event1", batch[0].GetProperty("event").GetString());
Assert.Equal("user1", batch[0].GetProperty("distinct_id").GetString());
Assert.Equal("event2", batch[1].GetProperty("event").GetString());
Assert.Equal("user2", batch[1].GetProperty("distinct_id").GetString());
}
}

Expand Down
Loading
Loading