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
68 changes: 68 additions & 0 deletions Assets/Tests/Editor/ExternalAssetFileStateComparerTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
using System;
using NUnit.Framework;

using io.github.hatayama.UnityCliLoop.FirstPartyTools;

namespace io.github.hatayama.UnityCliLoop.Tests.Editor
{
/// <summary>
/// Tests the shared external asset file-state identity contract.
/// </summary>
public sealed class ExternalAssetFileStateComparerTests
{
private static readonly DateTime SavedTime =
new DateTime(2026, 1, 1, 0, 0, 0, DateTimeKind.Utc);

/// <summary>
/// Verifies file states match when every fingerprint field is equal.
/// </summary>
[Test]
public void HasSameFileState_WhenAllFieldsMatch_ReturnsTrue()
{
bool hasSameFileState = ExternalAssetFileStateComparer.HasSameFileState(
(true, SavedTime, 10),
(true, SavedTime, 10));

Assert.That(hasSameFileState, Is.True);
}

/// <summary>
/// Verifies file states differ when only file existence changes.
/// </summary>
[Test]
public void HasSameFileState_WhenExistenceDiffers_ReturnsFalse()
{
bool hasSameFileState = ExternalAssetFileStateComparer.HasSameFileState(
(true, SavedTime, 10),
(false, SavedTime, 10));

Assert.That(hasSameFileState, Is.False);
}

/// <summary>
/// Verifies file states differ when only the last write time changes.
/// </summary>
[Test]
public void HasSameFileState_WhenLastWriteTimeDiffers_ReturnsFalse()
{
bool hasSameFileState = ExternalAssetFileStateComparer.HasSameFileState(
(true, SavedTime, 10),
(true, SavedTime.AddMinutes(1), 10));

Assert.That(hasSameFileState, Is.False);
}

/// <summary>
/// Verifies file states differ when only the file length changes.
/// </summary>
[Test]
public void HasSameFileState_WhenLengthDiffers_ReturnsFalse()
{
bool hasSameFileState = ExternalAssetFileStateComparer.HasSameFileState(
(true, SavedTime, 10),
(true, SavedTime, 20));

Assert.That(hasSameFileState, Is.False);
}
}
}
11 changes: 11 additions & 0 deletions Assets/Tests/Editor/ExternalAssetFileStateComparerTests.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
using System;

namespace io.github.hatayama.UnityCliLoop.FirstPartyTools
{
/// <summary>
/// Compares external asset fingerprints using the shared file-state identity contract.
/// </summary>
internal static class ExternalAssetFileStateComparer
{
internal static bool HasSameFileState(
(bool Exists, DateTime LastWriteTimeUtc, long Length) previousFingerprint,
(bool Exists, DateTime LastWriteTimeUtc, long Length) currentFingerprint)
{
return previousFingerprint.Exists == currentFingerprint.Exists &&
previousFingerprint.LastWriteTimeUtc == currentFingerprint.LastWriteTimeUtc &&
previousFingerprint.Length == currentFingerprint.Length;
}
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
using System;
using UnityEngine;

namespace io.github.hatayama.UnityCliLoop.FirstPartyTools
{
/// <summary>
/// Coordinates Auto Refresh suspension while Unity is unfocused.
/// </summary>
internal sealed class ExternalAssetFocusReturnService
{
private readonly Func<bool> _getAutoRefreshHeld;
private readonly Action<bool> _setAutoRefreshHeld;
private readonly Func<bool> _isEditorFocused;
private readonly Action _disallowAutoRefresh;
private readonly Action _allowAutoRefresh;
private readonly Action _resolveFocusReturnChanges;

internal ExternalAssetFocusReturnService(
Func<bool> getAutoRefreshHeld,
Action<bool> setAutoRefreshHeld,
Func<bool> isEditorFocused,
Action disallowAutoRefresh,
Action allowAutoRefresh,
Action resolveFocusReturnChanges)
{
Debug.Assert(getAutoRefreshHeld != null, "getAutoRefreshHeld must not be null");
Debug.Assert(setAutoRefreshHeld != null, "setAutoRefreshHeld must not be null");
Debug.Assert(isEditorFocused != null, "isEditorFocused must not be null");
Debug.Assert(disallowAutoRefresh != null, "disallowAutoRefresh must not be null");
Debug.Assert(allowAutoRefresh != null, "allowAutoRefresh must not be null");
Debug.Assert(resolveFocusReturnChanges != null, "resolveFocusReturnChanges must not be null");

_getAutoRefreshHeld = getAutoRefreshHeld ?? throw new ArgumentNullException(nameof(getAutoRefreshHeld));
_setAutoRefreshHeld = setAutoRefreshHeld ?? throw new ArgumentNullException(nameof(setAutoRefreshHeld));
_isEditorFocused = isEditorFocused ?? throw new ArgumentNullException(nameof(isEditorFocused));
_disallowAutoRefresh = disallowAutoRefresh ?? throw new ArgumentNullException(nameof(disallowAutoRefresh));
_allowAutoRefresh = allowAutoRefresh ?? throw new ArgumentNullException(nameof(allowAutoRefresh));
_resolveFocusReturnChanges =
resolveFocusReturnChanges ?? throw new ArgumentNullException(nameof(resolveFocusReturnChanges));
}

internal bool RestoreAutoRefreshIfHeld()
{
if (!_getAutoRefreshHeld())
{
return false;
}

if (!_isEditorFocused())
{
return false;
}

HandleFocusChanged(true);
return true;
}

internal void HandleFocusChanged(bool isFocused)
{
if (!isFocused)
{
HoldAutoRefreshIfNeeded();
return;
}

try
{
_resolveFocusReturnChanges();
}
finally
{
ReleaseAutoRefreshIfHeld();
}
}

private void HoldAutoRefreshIfNeeded()
{
if (_getAutoRefreshHeld())
{
return;
}

_disallowAutoRefresh();
_setAutoRefreshHeld(true);
}

private void ReleaseAutoRefreshIfHeld()
{
if (!_getAutoRefreshHeld())
{
return;
}

_allowAutoRefresh();
_setAutoRefreshHeld(false);
}
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
using System;
using System.Collections.Generic;
using UnityEngine;

namespace io.github.hatayama.UnityCliLoop.FirstPartyTools
{
/// <summary>
/// Serializes tracked asset fingerprints so focus-return recovery survives editor domain reloads.
/// </summary>
internal static class ExternalAssetSnapshotSessionStore
{
internal static string SerializeSnapshots(
Dictionary<string, (bool Exists, DateTime LastWriteTimeUtc, long Length)> snapshots)
{
Debug.Assert(snapshots != null, "snapshots must not be null");

AssetSnapshotSessionData data = new AssetSnapshotSessionData();
data.Entries = new AssetSnapshotEntry[snapshots.Count];
int index = 0;
foreach (KeyValuePair<string, (bool Exists, DateTime LastWriteTimeUtc, long Length)> snapshot in snapshots)
{
data.Entries[index] = new AssetSnapshotEntry
{
AssetPath = snapshot.Key,
Exists = snapshot.Value.Exists,
LastWriteTimeUtcTicks = snapshot.Value.LastWriteTimeUtc.Ticks,
Length = snapshot.Value.Length
};
index++;
}

return JsonUtility.ToJson(data);
}

internal static void RestoreSnapshots(
Dictionary<string, (bool Exists, DateTime LastWriteTimeUtc, long Length)> snapshots,
string json)
{
Debug.Assert(snapshots != null, "snapshots must not be null");

snapshots.Clear();
if (string.IsNullOrEmpty(json))
{
return;
}

AssetSnapshotSessionData data = JsonUtility.FromJson<AssetSnapshotSessionData>(json);
if (data == null || data.Entries == null)
{
return;
}

for (int i = 0; i < data.Entries.Length; i++)
{
AssetSnapshotEntry entry = data.Entries[i];
if (entry == null || string.IsNullOrEmpty(entry.AssetPath))
{
continue;
}

snapshots[entry.AssetPath] = (
entry.Exists,
new DateTime(entry.LastWriteTimeUtcTicks, DateTimeKind.Utc),
entry.Length);
}
}

[Serializable]
private sealed class AssetSnapshotSessionData
{
public AssetSnapshotEntry[] Entries = new AssetSnapshotEntry[0];
}

[Serializable]
private sealed class AssetSnapshotEntry
{
public string AssetPath;
public bool Exists;
public long LastWriteTimeUtcTicks;
public long Length;
}
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading