Skip to content

Conversation

@CoderGamester
Copy link
Owner

@CoderGamester CoderGamester commented Jan 11, 2026

New:

  • Added AGENTS.md document to help guide AI coding assistants to understand and work with this package library
  • Added an entire test suit of unit/integration/performance/smoke tests to cover all the code for all services in this package

Changed:

  • Changed VersionServices namespace from GameLovers to GameLovers.Services to maintain consistency with other services in the package.
  • Made CallOnSpawned, CallOnSpawned<TData>, and CallOnDespawned methods virtual in ObjectPoolBase<T> to allow derived pool classes to customize lifecycle callback behavior.

Fixed:

  • Fixed the README.md file to now follow best practices in OSS standards for Unity's package library projects
  • Fixed linter warnings in VersionServices.cs (redundant field initialization, unused lambda parameter, member shadowing)
  • Fixed GameObjectPool not invoking IPoolEntitySpawn.OnSpawn() and IPoolEntityDespawn.OnDespawn() on components attached to spawned GameObjects.

Summary by CodeRabbit

  • New Feature: Added comprehensive test suite for improved reliability.
  • Refactor: Made methods virtual in ObjectPoolBase for customization.
  • Performance: Optimized list iteration in TickService for efficiency.
  • Test: Introduced performance tests for MessageBrokerService and ObjectPool.
  • Test: Added unit tests for various services and GameObjectPool functionality.

Miguel Cartier and others added 2 commits December 27, 2025 18:22
- Added *AGENTS.md* document to help guide AI coding assistants to understand and work with this package library
- Added an entire test suit of unit/integration/performance/smoke tests to cover all the code for all services in this package

**Changed**:
- Changed *VersionServices* namespace from *GameLovers* to *GameLovers.Services* to maintain consistency with other services in the package.
- Made *CallOnSpawned*, *CallOnSpawned\<TData\>*, and *CallOnDespawned* methods virtual in *ObjectPoolBase\<T\>* to allow derived pool classes to customize lifecycle callback behavior.

**Fixed**:
- Fixed the *README.md* file to now follow best practices in OSS standards for Unity's package library projects
- Fixed linter warnings in *VersionServices.cs* (redundant field initialization, unused lambda parameter, member shadowing)
- Fixed *GameObjectPool* not invoking *IPoolEntitySpawn.OnSpawn()* and *IPoolEntityDespawn.OnDespawn()* on components attached to spawned GameObjects.
@coderabbitai
Copy link

coderabbitai bot commented Jan 11, 2026

Important

Review skipped

Auto reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch develop

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions
Copy link

github-actions bot commented Jan 11, 2026

Image description CodeRabbit

Walkthrough

The changes include enhancements across various files such as introducing new documents, optimizing methods for better customization and performance, adding unit and performance tests, and refining variable initialization and event handling.

Changes

File Summary
CHANGELOG.md Additions of AGENTS.md document and a comprehensive test suite. Namespace change in VersionServices, virtual methods added to ObjectPoolBase<T>, and fixes in README.md and VersionServices.cs. Also, a fix for GameObjectPool not invoking certain methods.
Editor/VersionEditorUtils.cs Introduce constants for file paths, modify GenerateInternalVersionSuffix to use CommitHash, and adjust file path operations in SaveVersionData.
Runtime/ObjectPool.cs, Runtime/TickService.cs Make certain methods virtual in ObjectPoolBase<T> for customization. Optimize iteration in TickService by iterating backwards for safe mutation and removing unnecessary array copying. Minor adjustments in variable initialization and event handling in VersionServices.cs.
Tests/EditMode/GameLovers.Services.Tests.asmdef, Tests/EditMode/Performance/MessageBrokerPerformanceTest.cs, Tests/EditMode/Performance/ObjectPoolPerformanceTest.cs, Tests/EditMode/Performance/PerformanceTestSetup.cs Add "Unity.PerformanceTesting" namespace. Introduce performance tests for MessageBrokerService and ObjectPool. Create PerformanceTestSetup class for setting up metadata. Add unit tests covering different functionalities.
Tests/Editor/EditMode/DataServiceTest.cs, Tests/Editor/EditMode/TimeServiceTest.cs Remove test classes related to data service testing and time conversions.
Tests/PlayMode/GameLovers.Services.Tests.Playmode.asmdef Add "Unity.PerformanceTesting" to dependencies. Introduce new test cases for service lifecycle testing, performance tests for GameObjectPool and TickService, and unit tests for various services including GameObjectPool and TickService.

In the realm of code, changes unfold,
A rabbit hops, the story is told.
Tests and tweaks, optimizations too,
Unity sings with a performance anew. 🐇🌟


Uplevel your code reviews with CodeRabbit Pro

CodeRabbit Pro

If you like this project, please support us by purchasing the Pro version. The Pro version has advanced context, superior noise reduction and several proprietary improvements compared to the open source version. Moreover, CodeRabbit Pro is free for open source projects.

Comment on lines +342 to +356
protected virtual void CallOnSpawned(T entity)
{
var poolEntity = entity as IPoolEntitySpawn;

poolEntity?.OnSpawn();
}

protected void CallOnSpawned<TData>(T entity, TData data)
protected virtual void CallOnSpawned<TData>(T entity, TData data)
{
var poolEntity = entity as IPoolEntitySpawn<TData>;

poolEntity?.OnSpawn(data);
}

protected void CallOnDespawned(T entity)
protected virtual void CallOnDespawned(T entity)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Image description CodeRabbit

  • The methods CallOnSpawned, CallOnSpawned<TData>, and CallOnDespawned have been changed to be protected virtual instead of just protected. This change allows derived pool classes to customize the lifecycle callback behavior, which is a good design decision for extensibility.

Comment on lines +436 to +458
/// <inheritdoc />
protected override void CallOnSpawned(GameObject entity)
{
var poolEntity = entity.GetComponent<IPoolEntitySpawn>();

poolEntity?.OnSpawn();
}

/// <inheritdoc />
protected override void CallOnSpawned<TData>(GameObject entity, TData data)
{
var poolEntity = entity.GetComponent<IPoolEntitySpawn<TData>>();

poolEntity?.OnSpawn(data);
}

/// <inheritdoc />
protected override void CallOnDespawned(GameObject entity)
{
var poolEntity = entity.GetComponent<IPoolEntityDespawn>();

poolEntity?.OnDespawn();
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Image description CodeRabbit

  • Similar changes have been made for methods related to GameObject entities. The CallOnSpawned, CallOnSpawned<TData>, and CallOnDespawned methods are now protected override, enabling customization of lifecycle callbacks for GameObject entities.
  • Additionally, a new method PostDespawnEntity has been added to handle post-despawn actions like deactivating the entity, which is a good practice for managing object states efficiently.

Comment on lines +545 to +568
/// <inheritdoc />
protected override void CallOnSpawned(T entity)
{
var poolEntity = entity.GetComponent<IPoolEntitySpawn>();

poolEntity?.OnSpawn();
}

/// <inheritdoc />
protected override void CallOnSpawned<TData>(T entity, TData data)
{
var poolEntity = entity.GetComponent<IPoolEntitySpawn<TData>>();

poolEntity?.OnSpawn(data);
}

/// <inheritdoc />
protected override void CallOnDespawned(T entity)
{
var poolEntity = entity.GetComponent<IPoolEntityDespawn>();

poolEntity?.OnDespawn();
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Image description CodeRabbit

  • The same changes as above have been applied to generic type T entities, ensuring consistency in handling lifecycle callbacks for different types of entities.
  • The addition of the PostDespawnEntity method for generic entities follows the same pattern as for GameObject entities, promoting code consistency.

Suggestions

No issues found in the provided code snippets.

Comment on lines +26 to +27
### Key Features

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Image description CodeRabbit

Consider optimizing the TickService class for safe iteration by using a foreach loop instead of directly iterating over the list. This can improve readability and potentially avoid issues related to modifying the collection during iteration.

- foreach (var tickable in _tickables)
+ foreach (var tickable in _tickables.ToList())

GameObjectPool.cs

Comment on lines +12 to +14

| Problem | Solution |
|---------|----------|

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Image description CodeRabbit

When adding virtual methods for customization, ensure that the base class provides a default implementation to avoid forcing derived classes to implement unnecessary behavior.

+ protected virtual void CallOnSpawned(T obj) { }
+ protected virtual void CallOnDespawned(T obj) { }

README.md

**Made with ❤️ for the Unity community**

*If this package helps your project, consider giving it a star ⭐ on GitHub!*
*If this package helps your project, please consider giving it a ⭐ on GitHub!*

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Image description CodeRabbit

Consider updating the message to encourage users to star the repository on GitHub for support and recognition.

*If this package helps your project, please consider giving it a ⭐ on GitHub!*

Overall

The code changes look mostly good with minor suggestions for optimization and consistency. Ensure to address the mentioned points to enhance performance, maintainability, and consistency across the codebase.

public struct TestMessage : IMessage {}

[Test, Performance]
public void Publish_100Subscribers_MeasureTime()

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Image description CodeRabbit

Consider adding comments to explain the purpose of the loop.

+ // Loop to subscribe mock subscribers for testing purposes

Comment on lines +26 to +29
{
var sub = new MockSubscriber();
broker.Subscribe<TestMessage>(sub.OnMessage);
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Image description CodeRabbit

Ensure proper cleanup after each test to avoid side effects on subsequent tests.

+ // Consider unsubscribing mock subscribers after each test to maintain test independence

Comment on lines +38 to +52
public void PublishSafe_MeasureAllocations()
{
var broker = new MessageBrokerService();
for (var i = 0; i < 100; i++)
{
var sub = new MockSubscriber();
broker.Subscribe<TestMessage>(sub.OnMessage);
}

Measure.Method(() => broker.PublishSafe(new TestMessage()))
.GC()
.WarmupCount(10)
.MeasurementCount(100)
.Run();
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Image description CodeRabbit

It's crucial to ensure that the performance tests are isolated and do not have side effects on memory allocations. Consider resetting or cleaning up resources between measurements.

+ // Add resource cleanup or reset mechanism between measurements to prevent interference

/// Call <see cref="Dispose"/> to clear the Tick Service correctly.
/// </summary>
public interface ITickService
public interface ITickService : IDisposable

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Image description CodeRabbit

The ITickService interface should inherit from IDisposable as mentioned in the description.

-	public interface ITickService
+	public interface ITickService : IDisposable

Issue in new hunk:


/// <inheritdoc cref="ITickService"/>
public class TickService : ITickService, IDisposable
public class TickService : ITickService

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Image description CodeRabbit

The TickService class should implement IDisposable as per the description.

-	public class TickService : ITickService
+	public class TickService : ITickService, IDisposable

Issue in new hunk:

Comment on lines +304 to +313
// Iterate backwards to allow safe mutation during iteration
for (int i = _onFixedUpdateList.Count - 1; i >= 0; i--)
{
arrayCopy[i].Action(Time.fixedTime);
// Skip if the item was removed by a previous action
if (i >= _onFixedUpdateList.Count)
{
continue;
}

_onFixedUpdateList[i].Action(Time.fixedTime);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Image description CodeRabbit

The iteration logic has changed significantly. The new implementation iterates backwards to allow safe mutation during iteration. However, there is a potential issue with the condition if (i >= _onFixedUpdateList.Count) which might lead to an out-of-bounds access. This condition needs to be revised to ensure it doesn't exceed the list size.

-			var arrayCopy = _onFixedUpdateList.ToArray();
-			for (int i = 0; i < arrayCopy.Length; i++)
+			for (int i = _onFixedUpdateList.Count - 1; i >= 0; i--)

Issue in new hunk:

Comment on lines 331 to 346
return;
}

var arrayCopy = list.ToArray();

for (int i = 0; i < arrayCopy.Length; i++)
// Iterate backwards to allow safe mutation during iteration
for (int i = list.Count - 1; i >= 0; i--)
{
var tickData = arrayCopy[i];
// Skip if the item was removed by a previous action
if (i >= list.Count)
{
continue;
}

var tickData = list[i];
var time = tickData.RealTime ? Time.realtimeSinceStartup : Time.time;

if (time < tickData.LastTickTime + tickData.DeltaTime)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Image description CodeRabbit

Note: This review was outside of the patch, so it was mapped to the patch with the greatest overlap. Original lines [334-360]

Similar to the previous iteration logic, the new implementation also iterates backwards for safe mutation. However, the condition if (i >= list.Count) needs to be adjusted to prevent out-of-bounds access.

-			var arrayCopy = list.ToArray();
-			for (int i = 0; i < arrayCopy.Length; i++)
+			for (int i = list.Count - 1; i >= 0; i--)

Issue in new hunk:

Comment on lines 352 to +360

tickData.Action(deltaTime);

// Check if the update was not unsubscribed in the call
var index = i - (arrayCopy.Length - countBefore);
if (list.Count > index && tickData == list[index])
// Check if the item still exists and wasn't unsubscribed
if (i < list.Count && list[i] == tickData)
{
var overFlow = tickData.DeltaTime == 0 ? 0 : deltaTime % tickData.DeltaTime;

tickData.LastTickTime = tickData.TimeOverflowToNextTick ? time - overFlow : time;

list[index] = tickData;
list[i] = tickData;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Image description CodeRabbit

The handling of the loop and updating the tickData.LastTickTime seems to have changed. Ensure that the logic for updating LastTickTime and checking if the item still exists is correctly implemented to avoid any issues related to data consistency.

No specific fix provided due to the complexity of changes.

---end_change_section---

Comment on lines +23 to +33
[Test, Performance]
public void ObjectPool_SpawnDespawn_1000Cycles()
{
var pool = new ObjectPool<MockEntity>(1000, () => new MockEntity());
var entities = new MockEntity[1000];

Measure.Method(() =>
{
for (var i = 0; i < 1000; i++) entities[i] = pool.Spawn();
for (var i = 0; i < 1000; i++) pool.Despawn(entities[i]);
})

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Image description CodeRabbit

The performance test for spawning and despawning entities in the ObjectPool class looks good. However, to further enhance the performance testing, consider adding assertions to validate the behavior of the pool after the cycles. This can help ensure that the pool is functioning correctly under load.

+ // Add assertions to validate pool state after spawn and despawn cycles
+ var activeCount = pool.ActiveCount;
+ var inactiveCount = pool.InactiveCount;
+ Assert.AreEqual(0, activeCount, "Active count should be 0 after despawning all entities");
+ Assert.AreEqual(1000, inactiveCount, "Inactive count should be 1000 after despawning all entities");

Comment on lines +13 to +19
public interface IInterface {}
public class Implementation : IInterface {}
public interface IDisposableInterface : IDisposable {}
public class DisposableImplementation : IDisposableInterface
{
public void Dispose() {}
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Image description CodeRabbit

Consider extracting the DisposableImplementation class into a separate file for better modularity and organization.

{
MainInstaller.Clean();
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Image description CodeRabbit

The Cleanup method should be marked as static since it does not access instance members.

Comment on lines +1 to +2
using System;
using GameLovers.Services;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Image description CodeRabbit

The using directive for System seems unnecessary and can be removed.

- using System;

Comment on lines +7 to 9
namespace GameLovers.Services
{
/// <summary>

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Image description CodeRabbit

The namespace change from GameLovers to GameLovers.Services seems inconsistent with the provided context. Ensure that this change aligns with the overall project structure and organization.

- namespace GameLovers
+ namespace GameLovers.Services

Inconsistent Field Name in VersionData Struct

Comment on lines +22 to 24
public string CommitHash;
public string BranchName;
public string BuildType;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Image description CodeRabbit

There is a discrepancy in the field name CommitHash in the VersionData struct compared to the old hunk where it was named Commit. Make sure the field names are consistent throughout the codebase for clarity and maintainability.

- public string Commit;
+ public string CommitHash;

Initialization of _loaded Field

55:
The _loaded field initialization seems to be missing in the new hunk. It's essential to ensure proper initialization of variables to avoid unexpected behavior.

+ private static bool _loaded;

Event Handler Assignment

69:
The event handler assignment for the request.completed event seems to have changed syntax. Verify that the event handling logic is correctly updated to prevent any unintended side effects.

- request.completed += operation => source.SetResult(request.asset as TextAsset);
+ request.completed += _ => source.SetResult(request.asset as TextAsset);

Version Formatting Logic

Comment on lines 124 to 130
/// </summary>
public static string FormatInternalVersion(VersionData data)
{
string version = $"{Application.version}-{data.BuildNumber}.{data.BranchName}.{data.Commit}";
var version = $"{Application.version}-{data.BuildNumber}.{data.BranchName}.{data.CommitHash}";

if (!string.IsNullOrEmpty(data.BuildType))
{

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Image description CodeRabbit

Note: This review was outside of the patch, so it was mapped to the patch with the greatest overlap. Original lines [127-131]

The version formatting logic has been modified. Ensure that the version string is constructed accurately based on the provided data fields to maintain consistency and correctness.

- string version = $"{Application.version}-{data.BuildNumber}.{data.BranchName}.{data.Commit}";
+ var version = $"{Application.version}-{data.BuildNumber}.{data.BranchName}.{data.CommitHash}";

Comment on lines +29 to +31
// Create and save run info to PlayerPrefs (required by Performance Testing Package in EditMode)
// Note: RunSettings is internal to the package, but only Run metadata is required to prevent
// the NullReferenceException in Metadata.SetRuntimeSettings()

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Image description CodeRabbit

Consider adding error handling for potential exceptions during PlayerPrefs saving.

+ try
+ {
    SaveToPrefs(run, PlayerPrefKeyRunJSON);
+ }
+ catch (Exception e)
+ {
+     Debug.LogError($"Error saving performance test metadata to PlayerPrefs: {e.Message}");
+ }

Comment on lines +54 to +79
private static Unity.PerformanceTesting.Data.Editor GetEditorInfo()
{
var fullVersion = UnityEditorInternal.InternalEditorUtility.GetFullUnityVersion();
const string pattern = @"(.+\.+.+\.\w+)|((?<=\().+(?=\)))";
var matches = Regex.Matches(fullVersion, pattern);

return new Unity.PerformanceTesting.Data.Editor
{
Branch = GetEditorBranch(),
Version = matches.Count > 0 ? matches[0].Value : "unknown",
Changeset = matches.Count > 1 ? matches[1].Value : "unknown",
Date = UnityEditorInternal.InternalEditorUtility.GetUnityVersionDate(),
};
}

private static string GetEditorBranch()
{
foreach (var method in typeof(UnityEditorInternal.InternalEditorUtility).GetMethods())
{
if (method.Name.Contains("GetUnityBuildBranch"))
{
return (string)method.Invoke(null, null);
}
}
return "null";
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Image description CodeRabbit

The method GetEditorBranch() could be simplified by directly returning the result of the method invocation.

-       foreach (var method in typeof(UnityEditorInternal.InternalEditorUtility).GetMethods())
-       {
-           if (method.Name.Contains("GetUnityBuildBranch"))
-           {
-               return (string)method.Invoke(null, null);
-           }
-       }
-       return "null";
+       var method = typeof(UnityEditorInternal.InternalEditorUtility).GetMethod("GetUnityBuildBranch");
+       return method?.Invoke(null, null)?.ToString() ?? "null";

Comment on lines +81 to +99
private static List<string> GetPackageDependencies()
{
var packages = UnityEditor.PackageManager.PackageInfo.GetAllRegisteredPackages();
return packages.Select(p => $"{p.name}@{p.version}").ToList();
}

private static void SetBuildSettings(Run run)
{
run.Player.GpuSkinning = PlayerSettings.gpuSkinning;
run.Player.ScriptingBackend = PlayerSettings
.GetScriptingBackend(UnityEditor.Build.NamedBuildTarget.FromBuildTargetGroup(EditorUserBuildSettings.selectedBuildTargetGroup))
.ToString();
run.Player.RenderThreadingMode = PlayerSettings.graphicsJobs
? PlayerSettings.graphicsJobMode.ToString()
: PlayerSettings.MTRendering ? "MultiThreaded" : "SingleThreaded";
run.Player.AndroidTargetSdkVersion = PlayerSettings.Android.targetSdkVersion.ToString();
run.Player.AndroidBuildSystem = EditorUserBuildSettings.androidBuildSystem.ToString();
run.Player.BuildTarget = EditorUserBuildSettings.activeBuildTarget.ToString();
run.Player.StereoRenderingPath = PlayerSettings.stereoRenderingPath.ToString();

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Image description CodeRabbit

The method SetBuildSettings() can be optimized by extracting repetitive code into separate methods for better readability and maintainability.

+ 		private static void SetPlayerSettings(Run run)
+ 		{
+ 			run.Player.GpuSkinning = PlayerSettings.gpuSkinning;
+ 			run.Player.AndroidTargetSdkVersion = PlayerSettings.Android.targetSdkVersion.ToString();
+ 			run.Player.AndroidBuildSystem = EditorUserBuildSettings.androidBuildSystem.ToString();
+ 			run.Player.BuildTarget = EditorUserBuildSettings.activeBuildTarget.ToString();
+ 			run.Player.StereoRenderingPath = PlayerSettings.stereoRenderingPath.ToString();
+ 		}
+
+ 		private static void SetScriptingBackend(Run run)
+ 		{
+ 			run.Player.ScriptingBackend = PlayerSettings.GetScriptingBackend(UnityEditor.Build.NamedBuildTarget.FromBuildTargetGroup(EditorUserBuildSettings.selectedBuildTargetGroup)).ToString();
+ 		}
+
+ 		private static void SetRenderThreadingMode(Run run)
+ 		{
+ 			run.Player.RenderThreadingMode = PlayerSettings.graphicsJobs ? PlayerSettings.graphicsJobMode.ToString() : PlayerSettings.MTRendering ? "MultiThreaded" : "SingleThreaded";
+ 		}
+
+ 		private static void SetBuildSettings(Run run)
+ 		{
+ 			SetPlayerSettings(run);
+ 			SetScriptingBackend(run);
+ 			SetRenderThreadingMode(run);
+ 		}

Comment on lines +102 to +106
private static long ConvertToUnixTimestamp(DateTime date)
{
var origin = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc);
var diff = date.ToUniversalTime() - origin;
return (long)Math.Floor(diff.TotalSeconds);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Image description CodeRabbit

Consider using TimeSpan.TotalSeconds instead of Math.Floor(diff.TotalSeconds) for converting to Unix timestamp.

-       return (long)Math.Floor(diff.TotalSeconds);
+       return (long)diff.TotalSeconds;

End of Review

Comment on lines +33 to +39
public void AddData_Successfully()
{
var data = Substitute.For<IDataMockup>();

_dataService.AddOrReplaceData(data);

Assert.AreSame(data, _dataService.GetData<IDataMockup>());

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Image description CodeRabbit

The test AddData_Successfully seems to be testing the behavior correctly. However, consider adding a teardown method to clean up after each test to ensure independence between tests.

[Test]
public void AddData_Successfully()
{
    var data = Substitute.For<IDataMockup>();
    
    _dataService.AddOrReplaceData(data);

    Assert.AreSame(data, _dataService.GetData<IDataMockup>());
}

[TearDown]
public void TearDown()
{
    PlayerPrefs.DeleteAll();
}

Comment on lines +22 to +26
public void DateTime_Convertions_Successfully()
{
Assert.GreaterOrEqual(ErrorValue, (_timeService.DateTimeUtcFromUnityTime(_timeService.UnityTimeNow) - _timeService.DateTimeUtcNow).TotalMilliseconds);
Assert.GreaterOrEqual(ErrorValue, (_timeService.DateTimeUtcFromUnixTime(_timeService.UnixTimeNow) - _timeService.DateTimeUtcNow).TotalMilliseconds);
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Image description CodeRabbit

In the DateTime_Convertions_Successfully test, consider breaking down the assertions into separate tests for better test granularity and readability.

Comment on lines +30 to +33
{
Assert.GreaterOrEqual(ErrorValue, _timeService.UnityTimeFromDateTimeUtc(_timeService.DateTimeUtcNow) - _timeService.UnityTimeNow);
Assert.GreaterOrEqual(ErrorValue, _timeService.UnityTimeFromUnixTime(_timeService.UnixTimeNow) - _timeService.UnityTimeNow);
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Image description CodeRabbit

Similarly, in the UnityTime_Convertions_Successfully test, split the assertions into individual tests for clarity and easier debugging.

Comment on lines +36 to +40
public void UnixTime_Convertions_Successfully()
{
Assert.GreaterOrEqual(ErrorValue, _timeService.UnixTimeFromDateTimeUtc(_timeService.DateTimeUtcNow) - _timeService.UnixTimeNow);
Assert.GreaterOrEqual(ErrorValue, _timeService.UnixTimeFromUnityTime(_timeService.UnityTimeNow) - _timeService.UnixTimeNow);
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Image description CodeRabbit

For the UnixTime_Convertions_Successfully test, it's advisable to separate each assertion into its own test method to isolate failures.

Comment on lines +43 to +56
public void AddTime_AllTimeTypes_Successfully()
{
var extraTime = 50.5f;
var extraTimeInMilliseconds = TimeSpan.FromSeconds(extraTime).TotalMilliseconds;
var dateTime = _timeService.DateTimeUtcNow;
var unityTime = _timeService.UnityTimeNow;
var unixTime = _timeService.UnixTimeNow;

_timeService.AddTime(extraTime);

Assert.LessOrEqual(0, _timeService.DateTimeUtcNow.CompareTo(dateTime.AddSeconds(extraTime)));
Assert.GreaterOrEqual(_timeService.UnityTimeNow, unityTime + extraTime);
Assert.GreaterOrEqual(_timeService.UnixTimeNow, unixTime - extraTimeInMilliseconds);
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Image description CodeRabbit

The AddTime_AllTimeTypes_Successfully test is doing multiple assertions within a single test. It's recommended to split these assertions into separate test methods to improve test maintainability and readability.

Comment on lines +59 to +68
public void AddTime_NegativeValue_SubtractsTime()
{
var initialUnityTime = _timeService.UnityTimeNow;
var negativeTime = -10f;

_timeService.AddTime(negativeTime);

Assert.Less(_timeService.UnityTimeNow, initialUnityTime);
Assert.That(_timeService.UnityTimeNow, Is.EqualTo(initialUnityTime + negativeTime).Within(ErrorValue));
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Image description CodeRabbit

In the AddTime_NegativeValue_SubtractsTime test, consider breaking down the assertions into distinct test cases to enhance test isolation and clarity.

Comment on lines +71 to +84
public void SetInitialTime_ResetsTimeBase()
{
// SetInitialTime acts as a "reset" by synchronizing the time base
var customInitialTime = new DateTime(2025, 1, 1, 12, 0, 0, DateTimeKind.Utc);

_timeService.SetInitialTime(customInitialTime);

// After setting initial time, DateTimeUtcNow should be close to the custom time
// (plus any time that has passed since realtimeSinceStartup was captured)
var now = _timeService.DateTimeUtcNow;

// The difference should be very small (just the time since SetInitialTime was called)
Assert.That((now - customInitialTime).TotalSeconds, Is.LessThan(1.0));
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Image description CodeRabbit

The SetInitialTime_ResetsTimeBase test contains comments that explain the purpose of the test. While comments are helpful, consider refactoring them into descriptive test method names to make the intent clearer without relying solely on comments.

Overall Review

The test class TimeServiceTest contains multiple test methods with grouped assertions. To enhance test readability, maintainability, and isolation, it's recommended to refactor the tests by splitting the grouped assertions into separate test methods. Additionally, consider using more descriptive test method names to convey the test scenarios effectively.

Comment on lines +12 to +39
{
public class MockPoolEntity : MonoBehaviour, IPoolEntitySpawn, IPoolEntityDespawn
{
public int SpawnCount;
public int DespawnCount;

public void OnSpawn() => SpawnCount++;
public void OnDespawn() => DespawnCount++;
}

private GameObject _sample;
private GameObjectPool _pool;

[SetUp]
public void Init()
{
_sample = new GameObject("Sample");
_sample.AddComponent<MockPoolEntity>();
_sample.SetActive(false);
_pool = new GameObjectPool(0, _sample);
}

[TearDown]
public void Cleanup()
{
_pool.Dispose(true);
if (_sample != null) Object.Destroy(_sample);
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Image description CodeRabbit

The setup and teardown methods in the test class are well-structured. However, there is a potential issue with object destruction timing in the Dispose_DestroysAllInstances test case. As noted in the comment, object destruction is delayed until the end of the frame or the next frame. This can lead to assertions failing due to the object still being present during the test execution.

+ // Consider waiting for one more frame before asserting instance destruction

Comment on lines +41 to +98
[UnityTest]
public IEnumerator Spawn_InstantiatesPrefab()
{
var instance = _pool.Spawn();

Assert.IsNotNull(instance);
Assert.AreNotSame(_sample, instance);
Assert.IsTrue(instance.activeSelf);

yield return null;
}

[UnityTest]
public IEnumerator Despawn_DeactivatesGameObject()
{
var instance = _pool.Spawn();
_pool.Despawn(instance);

Assert.IsFalse(instance.activeSelf);

yield return null;
}

[UnityTest]
public IEnumerator Spawn_InvokesIPoolEntitySpawn()
{
var instance = _pool.Spawn();
var mock = instance.GetComponent<MockPoolEntity>();

Assert.AreEqual(1, mock.SpawnCount);

_pool.Despawn(instance);
Assert.AreEqual(1, mock.DespawnCount);

yield return null;
}

[UnityTest]
public IEnumerator Dispose_DestroysAllInstances()
{
var instance = _pool.Spawn();
_pool.Dispose();

// Note: Object destruction is delayed until end of frame or next frame
yield return null;

Assert.IsTrue(instance == null);
}

[UnityTest]
public IEnumerator Dispose_WithSampleDestroy_DestroysSample()
{
_pool.Dispose(true);

yield return null;

Assert.IsTrue(_sample == null);
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Image description CodeRabbit

The test cases cover various aspects of the GameObjectPool functionality effectively. However, it's crucial to ensure that the tests are deterministic and do not rely on timing or external factors that may introduce flakiness.

+ // Ensure tests are deterministic and not affected by timing issues

End of Review

Comment on lines +60 to +63
var entity = _poolService.Spawn<IMockPoolableEntity>();

Assert.AreEqual(entity,_poolService.Spawn<IMockPoolableEntity>());
_pool.Received().Spawn();
Assert.IsNotNull(entity);
Assert.IsInstanceOf<MockPoolableEntity>(entity);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Image description CodeRabbit

The new implementation directly spawns an entity from _poolService, which is a more straightforward approach compared to using a substitute. This change simplifies the test setup and assertion.

-			var entity = Substitute.For<IMockPoolableEntity>();
-			_pool.Spawn().Returns(entity);
-			
-			Assert.AreEqual(entity, _poolService.Spawn<IMockPoolableEntity>());
-			_pool.Received().Spawn();
+			var entity = _poolService.Spawn<IMockPoolableEntity>();
+			
+			Assert.IsNotNull(entity);
+			Assert.IsInstanceOf<MockPoolableEntity>(entity);

Comment on lines +77 to +79
var entity = _poolService.Spawn<IMockPoolableEntity>();

_poolService.Despawn(entity);
_pool.Received().Despawn(entity);
Assert.DoesNotThrow(() => _poolService.Despawn(entity));

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Image description CodeRabbit

In the new implementation, the test now directly spawns an entity from _poolService and asserts that despawning the entity does not throw an exception. This change simplifies the test logic.

-			var entity = Substitute.For<IMockPoolableEntity>();
- 			
-			_poolService.Despawn(entity);
- 			
-			_pool.Received().Despawn(entity);
+			var entity = _poolService.Spawn<IMockPoolableEntity>();
+			
+			Assert.DoesNotThrow(() => _poolService.Despawn(entity));

Comment on lines 83 to +85
public void Despawn_NotAddedPool_ThrowsException()
{
var entity = Substitute.For<IMockPoolableEntity>();
var entity = new MockPoolableEntity();

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Image description CodeRabbit

The new test creates a new MockPoolableEntity directly instead of using a substitute. This change simplifies the test setup and aligns with the direct usage of entities in the pool service.

-			var entity = Substitute.For<IMockPoolableEntity>();
- 			
- 			_poolService = new PoolService();
+			var entity = new MockPoolableEntity();
+ 			
+ 			_poolService = new PoolService();

Comment on lines +95 to +98
_poolService.Spawn<IMockPoolableEntity>();
_poolService.DespawnAll<IMockPoolableEntity>();
_pool.Received().DespawnAll();
Assert.DoesNotThrow(() => _poolService.DespawnAll<IMockPoolableEntity>());

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Image description CodeRabbit

The updated test now directly spawns an entity and tests the DespawnAll method on _poolService. This change simplifies the test by removing unnecessary interactions with the pool object.

- 			_poolService.DespawnAll<IMockPoolableEntity>();
-
-			_pool.Received().DespawnAll();
+			_poolService.Spawn<IMockPoolableEntity>();
+  			_poolService.DespawnAll<IMockPoolableEntity>();
+ 			
+			Assert.DoesNotThrow(() => _poolService.DespawnAll<IMockPoolableEntity>());

Comment on lines 1 to 4
using System;
using System.Collections.Generic;
using System.Threading;
using GameLovers.Services;
using NSubstitute;
using NUnit.Framework;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Image description CodeRabbit

Note: This review was outside of the patch, but no patch was found that overlapped with it. Original lines [106-109]

The new test ensures that calling RemovePool on _poolService does not throw an exception. This test verifies the removal functionality directly without involving the pool object, which is a good practice for unit testing.

+			Assert.DoesNotThrow(() => _poolService.RemovePool<IMockPoolableEntity>());

Comment on lines +15 to +28
public IEnumerator Update_1000Subscribers_MeasureFrameTime()
{
var tickService = new TickService();
for (var i = 0; i < 1000; i++)
{
tickService.SubscribeOnUpdate(dt => {});
}

yield return Measure.Frames()
.WarmupCount(10)
.MeasurementCount(100)
.Run();

tickService.Dispose();

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Image description CodeRabbit

The performance test Update_1000Subscribers_MeasureFrameTime seems to be subscribing 1000 times within a loop. This could potentially impact performance due to the high number of subscriptions. Consider optimizing this logic to avoid unnecessary subscriptions.

Comment on lines +31 to +48
[Test, Performance]
public void Subscribe_Unsubscribe_Churn_MeasureTime()
{
var tickService = new TickService();
System.Action<float>[] actions = new System.Action<float>[100];
for (int i = 0; i < 100; i++) actions[i] = dt => {};

Measure.Method(() =>
{
for (var i = 0; i < 100; i++) tickService.SubscribeOnUpdate(actions[i]);
for (var i = 0; i < 100; i++) tickService.UnsubscribeOnUpdate(actions[i]);
})
.WarmupCount(5)
.MeasurementCount(20)
.Run();

tickService.Dispose();
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Image description CodeRabbit

In the Subscribe_Unsubscribe_Churn_MeasureTime test, there is a loop subscribing and unsubscribing actions. While this churn test is valid, ensure that the actions being subscribed and unsubscribed are unique to simulate real-world scenarios accurately. Also, consider adding assertions to validate the behavior of the TickService.

+ // Consider adding assertions to validate behavior
+ Assert.AreEqual(expectedResult, actualResult);

Comment on lines +15 to +28
public IEnumerator Update_1000Subscribers_MeasureFrameTime()
{
var tickService = new TickService();
for (var i = 0; i < 1000; i++)
{
tickService.SubscribeOnUpdate(dt => {});
}

yield return Measure.Frames()
.WarmupCount(10)
.MeasurementCount(100)
.Run();

tickService.Dispose();

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Image description CodeRabbit

The performance test Update_1000Subscribers_MeasureFrameTime seems to be subscribing 1000 times within a loop. This could potentially impact performance due to the high number of subscriptions. Consider optimizing this logic to avoid unnecessary subscriptions.

- for (var i = 0; i < 1000; i++)
+ for (var i = 0; i < 1000; i+=10) // Example: Subscribe in batches of 10

Comment on lines +31 to +48
[Test, Performance]
public void Subscribe_Unsubscribe_Churn_MeasureTime()
{
var tickService = new TickService();
System.Action<float>[] actions = new System.Action<float>[100];
for (int i = 0; i < 100; i++) actions[i] = dt => {};

Measure.Method(() =>
{
for (var i = 0; i < 100; i++) tickService.SubscribeOnUpdate(actions[i]);
for (var i = 0; i < 100; i++) tickService.UnsubscribeOnUpdate(actions[i]);
})
.WarmupCount(5)
.MeasurementCount(20)
.Run();

tickService.Dispose();
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Image description CodeRabbit

In the Subscribe_Unsubscribe_Churn_MeasureTime test, there is a loop subscribing and unsubscribing actions. While this churn test is valid, ensure that the actions being subscribed and unsubscribed are unique to simulate real-world scenarios accurately. Also, consider adding assertions to validate the behavior of the TickService.

+ // Consider adding assertions to validate behavior
+ Assert.AreEqual(expectedResult, actualResult);

Comment on lines +28 to +36
public IEnumerator SubscribeOnUpdate_ReceivesDeltaTime()
{
float receivedDelta = -1f;
_tickService.SubscribeOnUpdate(dt => receivedDelta = dt);

yield return null; // Wait for next frame
yield return null; // Wait one more to be sure

Assert.GreaterOrEqual(receivedDelta, 0f);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Image description CodeRabbit

Consider adding a timeout mechanism to prevent the test from hanging indefinitely in case of unexpected behavior.

Comment on lines +54 to +64
public IEnumerator SubscribeOnUpdate_TimeOverflow_CarriesOverflow()
{
float interval = 0.05f;
int callCount = 0;
_tickService.SubscribeOnUpdate(dt => callCount++, interval, true);

yield return new WaitForSeconds(interval * 2.5f);

// If overflow is carried, it should have triggered at least twice
Assert.GreaterOrEqual(callCount, 2);
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Image description CodeRabbit

Ensure that the time-based tests are resilient to potential variations in execution time, especially on different machines or under varying loads.

Comment on lines +66 to +80
[UnityTest]
public IEnumerator SubscribeOnUpdate_RealTime_UsesUnscaledTime()
{
float initialTimeScale = Time.timeScale;
Time.timeScale = 0f;

float receivedDelta = -1f;
_tickService.SubscribeOnUpdate(dt => receivedDelta = dt, 0f, false, true);

yield return new WaitForSecondsRealtime(0.1f);

Time.timeScale = initialTimeScale;

Assert.Greater(receivedDelta, 0f);
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Image description CodeRabbit

The test involving real-time and unscaled time should be carefully reviewed to handle edge cases where time might not progress as expected due to system limitations.

Comment on lines +82 to +99
[UnityTest]
public IEnumerator UnsubscribeOnUpdate_DuringCallback_SafelyRemoves()
{
int callCount = 0;
System.Action<float> action = null;
action = dt =>
{
callCount++;
_tickService.UnsubscribeOnUpdate(action);
};

_tickService.SubscribeOnUpdate(action);

yield return null;
yield return null;

Assert.AreEqual(1, callCount);
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Image description CodeRabbit

The test for unsubscribing during a callback should be expanded to cover scenarios where multiple subscribers are involved to ensure robustness.

Comment on lines +123 to +134
public IEnumerator Dispose_DestroysGameObject()
{
var initialCount = Object.FindObjectsByType<TickServiceMonoBehaviour>(FindObjectsSortMode.None).Length;
var tickService = new TickService();

Assert.AreEqual(initialCount + 1, Object.FindObjectsByType<TickServiceMonoBehaviour>(FindObjectsSortMode.None).Length);

tickService.Dispose();
yield return null; // Allow Destroy to complete

Assert.AreEqual(initialCount, Object.FindObjectsByType<TickServiceMonoBehaviour>(FindObjectsSortMode.None).Length);
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Image description CodeRabbit

It's recommended to verify the disposal of GameObjects more thoroughly by checking if the disposed objects are actually destroyed in Unity's scene hierarchy.

Comment on lines +137 to +163
public void MultipleInstances_CreateMultipleGameObjects()
{
// Note: The service doesn't enforce singleton, but it throws if _tickObject is already set
// However, _tickObject is an instance field in the current implementation.
// Wait, I saw a check in the constructor:
/*
public TickService()
{
if (_tickObject != null)
{
throw new InvalidOperationException("The tick service is being initialized for the second time and that is not valid");
}
...
}
*/
// But _tickObject is private readonly TickServiceMonoBehaviour _tickObject;
// So it's always null for a new instance. The check seems to be intended for a static field but isn't.

var service1 = new TickService();
var service2 = new TickService();

var objects = Object.FindObjectsByType<TickServiceMonoBehaviour>(FindObjectsSortMode.None);
Assert.GreaterOrEqual(objects.Length, 2);

service1.Dispose();
service2.Dispose();
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Image description CodeRabbit

The test for creating multiple instances should be revised to accurately reflect the intended behavior regarding singleton enforcement and object creation.

This review provides insights into potential improvements and areas to consider for enhancing the reliability and effectiveness of the test suite.

Comment on lines +20 to +26
public void AllServices_Instantiate_WithoutException()
{
Assert.DoesNotThrow(() => new MessageBrokerService());
Assert.DoesNotThrow(() => new PoolService());
Assert.DoesNotThrow(() => new DataService());
Assert.DoesNotThrow(() => new TimeService());
Assert.DoesNotThrow(() => new RngService(RngService.CreateRngData(0)));

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Image description CodeRabbit

Consider consolidating the instantiation tests for services into a parameterized test to reduce duplication and improve maintainability.

[Test]
public void Services_Instantiate_WithoutException([ValueSource(typeof(ServiceSource), "All")] Type serviceType)
{
    Assert.DoesNotThrow(() => Activator.CreateInstance(serviceType));
}

Comment on lines +30 to +35
public void TickService_CreatesGameObject()
{
var service = new TickService();
var go = GameObject.Find("TickServiceMonoBehaviour");
Assert.IsNotNull(go);
service.Dispose();

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Image description CodeRabbit

Ensure proper cleanup of GameObject instances created during tests to avoid potential memory leaks or clutter in the scene.

[Test]
public void TickService_CreatesGameObject()
{
    var service = new TickService();
    var go = GameObject.Find("TickServiceMonoBehaviour");
    Assert.IsNotNull(go);
    service.Dispose();
    GameObject.Destroy(go); // Add this line to properly clean up the GameObject.
}

Comment on lines +38 to +44
[Test]
public void CoroutineService_CreatesGameObject()
{
var service = new CoroutineService();
var go = GameObject.Find("CoroutineServiceMonoBehaviour");
Assert.IsNotNull(go);
service.Dispose();

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Image description CodeRabbit

Apply the same cleanup pattern as above to the CoroutineService test to ensure proper disposal of resources.

Comment on lines +47 to +53
[Test]
public void MainInstaller_BindResolve_Works()
{
var broker = new MessageBrokerService();
MainInstaller.Bind<IMessageBrokerService>(broker);
Assert.AreSame(broker, MainInstaller.Resolve<IMessageBrokerService>());
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Image description CodeRabbit

Consider refactoring the MainInstaller_BindResolve_Works test to use Arrange-Act-Assert (AAA) pattern for better readability and maintainability.

[Test]
public void MainInstaller_BindResolve_Works()
{
    // Arrange
    var broker = new MessageBrokerService();

    // Act
    MainInstaller.Bind<IMessageBrokerService>(broker);

    // Assert
    Assert.AreSame(broker, MainInstaller.Resolve<IMessageBrokerService>());
}

@CoderGamester CoderGamester merged commit 9f02bca into master Jan 11, 2026
2 checks passed
Repository owner deleted a comment from github-actions bot Jan 11, 2026
Repository owner deleted a comment from github-actions bot Jan 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants