845 lines
31 KiB
C#
845 lines
31 KiB
C#
using System;
|
|
using System.Globalization;
|
|
using System.IO;
|
|
using System.Security.Cryptography;
|
|
using NUnit.Framework;
|
|
using UnityEngine;
|
|
|
|
namespace Streamingle.Editor.Tests
|
|
{
|
|
public sealed class AICameraGeneratorWindowTests
|
|
{
|
|
[Serializable]
|
|
private sealed class DirectiveDocument
|
|
{
|
|
public string schemaVersion;
|
|
public DirectiveShot[] shots;
|
|
}
|
|
|
|
[Serializable]
|
|
private sealed class DirectiveShot
|
|
{
|
|
public int index;
|
|
public string shotSize;
|
|
public string motion;
|
|
public string composition;
|
|
public float distanceMeters;
|
|
public float motionIntensity;
|
|
public float bodyFollowStrength;
|
|
}
|
|
|
|
private static string HybridMetadataJson(
|
|
string generationMode = "hierarchical_hybrid_retarget_v12",
|
|
string plannerMode = "hierarchical",
|
|
double targetAspectRatio = 16d / 9d)
|
|
{
|
|
return "{" +
|
|
"\"schemaVersion\":\"3.6\"," +
|
|
$"\"generationMode\":\"{generationMode}\"," +
|
|
"\"songId\":\"song-id\"," +
|
|
"\"datasetId\":\"dataset-id\"," +
|
|
"\"frameCount\":6," +
|
|
"\"sampleRate\":60," +
|
|
"\"shotCount\":2," +
|
|
$"\"plannerMode\":\"{plannerMode}\"," +
|
|
"\"targetAspectRatio\":" +
|
|
targetAspectRatio.ToString(
|
|
"R",
|
|
CultureInfo.InvariantCulture) + "," +
|
|
"\"worldCameraFile\":\"generated_camera_world.f32\"," +
|
|
"\"timeFile\":\"time.f64\"," +
|
|
"\"shotsFile\":\"shots.json\"," +
|
|
"\"outputSha256\":{" +
|
|
"\"worldCamera\":\"camera-hash\"," +
|
|
"\"time\":\"time-hash\"," +
|
|
"\"shots\":\"shots-hash\"}}";
|
|
}
|
|
|
|
private static string HybridShotsJson(string schemaVersion = "1.2")
|
|
{
|
|
return "{" +
|
|
$"\"schemaVersion\":\"{schemaVersion}\"," +
|
|
"\"shots\":[" +
|
|
"{\"index\":0,\"startFrame\":0,\"endFrameExclusive\":3}," +
|
|
"{\"index\":1,\"startFrame\":3,\"endFrameExclusive\":6}]}";
|
|
}
|
|
|
|
[Test]
|
|
public void DefaultCwAiRootPrefersConfiguredThenSiblingRepository()
|
|
{
|
|
var root = Path.Combine(
|
|
Path.GetTempPath(),
|
|
"cw-ai-root-tests-" + Guid.NewGuid().ToString("N"));
|
|
var project = Path.Combine(root, "UnityProject");
|
|
var configured = Path.Combine(root, "ConfiguredCWAI");
|
|
var sibling = Path.Combine(root, "CW-AI");
|
|
Directory.CreateDirectory(project);
|
|
CreateCwAiRootMarker(configured);
|
|
CreateCwAiRootMarker(sibling);
|
|
try
|
|
{
|
|
Assert.That(
|
|
AICameraGeneratorWindow.ResolveDefaultCwAiRoot(
|
|
project,
|
|
configured),
|
|
Is.EqualTo(Path.GetFullPath(configured)));
|
|
Assert.That(
|
|
AICameraGeneratorWindow.ResolveDefaultCwAiRoot(
|
|
project,
|
|
string.Empty),
|
|
Is.EqualTo(Path.GetFullPath(sibling)));
|
|
}
|
|
finally
|
|
{
|
|
Directory.Delete(root, true);
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
public void DefaultCwAiRootSkipsProjectCopyWithoutTrainingIndex()
|
|
{
|
|
var root = Path.Combine(
|
|
Path.GetTempPath(),
|
|
"cw-ai-incomplete-root-tests-" + Guid.NewGuid().ToString("N"));
|
|
var project = Path.Combine(root, "UnityProject");
|
|
var sibling = Path.Combine(root, "CW-AI");
|
|
Directory.CreateDirectory(Path.Combine(project, "DatasetExports"));
|
|
Directory.CreateDirectory(Path.Combine(
|
|
project,
|
|
"MachineLearning",
|
|
"CameraDirector"));
|
|
CreateCwAiRootMarker(sibling);
|
|
try
|
|
{
|
|
Assert.That(
|
|
AICameraGeneratorWindow.IsGenerationLibraryRoot(project),
|
|
Is.False);
|
|
Assert.That(
|
|
AICameraGeneratorWindow.ResolveDefaultCwAiRoot(
|
|
project,
|
|
project),
|
|
Is.EqualTo(Path.GetFullPath(sibling)));
|
|
}
|
|
finally
|
|
{
|
|
Directory.Delete(root, true);
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
public void DefaultCwAiRootSkipsMalformedConfiguredPath()
|
|
{
|
|
var root = Path.Combine(
|
|
Path.GetTempPath(),
|
|
"cw-ai-malformed-root-tests-" + Guid.NewGuid().ToString("N"));
|
|
var project = Path.Combine(root, "UnityProject");
|
|
var sibling = Path.Combine(root, "CW-AI");
|
|
Directory.CreateDirectory(project);
|
|
CreateCwAiRootMarker(sibling);
|
|
try
|
|
{
|
|
Assert.That(
|
|
AICameraGeneratorWindow.ResolveDefaultCwAiRoot(
|
|
project,
|
|
"\0invalid"),
|
|
Is.EqualTo(Path.GetFullPath(sibling)));
|
|
}
|
|
finally
|
|
{
|
|
Directory.Delete(root, true);
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
public void ProjectCacheRootDefaultsToWritableProjectLibrary()
|
|
{
|
|
var project = Path.Combine(
|
|
Path.GetTempPath(),
|
|
"cw-ai-cache-root-tests-" + Guid.NewGuid().ToString("N"));
|
|
var configured = Path.Combine(project, "ConfiguredCache");
|
|
|
|
Assert.That(
|
|
AICameraGeneratorWindow.ResolveProjectCacheRoot(
|
|
project,
|
|
string.Empty),
|
|
Is.EqualTo(Path.Combine(
|
|
Path.GetFullPath(project),
|
|
"Library",
|
|
"CWAI")));
|
|
Assert.That(
|
|
AICameraGeneratorWindow.ResolveProjectCacheRoot(
|
|
project,
|
|
configured),
|
|
Is.EqualTo(Path.GetFullPath(configured)));
|
|
}
|
|
|
|
private static void CreateCwAiRootMarker(string root)
|
|
{
|
|
Directory.CreateDirectory(Path.Combine(root, "DatasetExports"));
|
|
Directory.CreateDirectory(Path.Combine(
|
|
root,
|
|
"MachineLearning",
|
|
"CameraDirector"));
|
|
var reports = Directory.CreateDirectory(Path.Combine(root, "reports"));
|
|
File.WriteAllText(
|
|
Path.Combine(reports.FullName, "training_index.json"),
|
|
"{}");
|
|
var models = Directory.CreateDirectory(Path.Combine(root, "models"));
|
|
File.WriteAllText(
|
|
Path.Combine(models.FullName, "cut_ranker_v2.json"),
|
|
"{}");
|
|
}
|
|
|
|
[Test]
|
|
public void CliWorkerIsTheOnlyHighQualityBackend()
|
|
{
|
|
Assert.That(
|
|
AICameraGeneratorWindow.HybridQualityBackendLabel,
|
|
Does.Contain("CWCameraWorker"));
|
|
Assert.That(
|
|
AICameraGeneratorWindow.HybridQualityBackendDescription,
|
|
Does.Contain("별도 Python 설치가 필요하지 않습니다"));
|
|
}
|
|
|
|
[TestCase(0, "auto")]
|
|
[TestCase(1, "slow")]
|
|
[TestCase(2, "normal")]
|
|
[TestCase(3, "fast")]
|
|
public void CutRhythmUsesStableDataPlannerCliValues(
|
|
int value,
|
|
string expected)
|
|
{
|
|
Assert.That(
|
|
AICameraGeneratorWindow.CutDensityCliValueForTests(value),
|
|
Is.EqualTo(expected));
|
|
Assert.That(
|
|
AICameraGeneratorWindow.CutRhythmExplanation,
|
|
Does.Contain("음악"));
|
|
}
|
|
|
|
[TestCase("auto", "balanced")]
|
|
[TestCase("center", "centered")]
|
|
[TestCase("centered", "centered")]
|
|
[TestCase("left_third", "left_third")]
|
|
[TestCase("right-third", "right_third")]
|
|
public void HybridHorizontalFramingPreservesExplicitDirection(
|
|
string composition,
|
|
string expected)
|
|
{
|
|
Assert.That(
|
|
AICameraGeneratorWindow.HybridHorizontalFraming(composition),
|
|
Is.EqualTo(expected));
|
|
}
|
|
|
|
[Test]
|
|
public void FullGenerationIgnoresPersistedManualOverrides()
|
|
{
|
|
var unusedOutput = Path.Combine(
|
|
Path.GetTempPath(),
|
|
"cw-ai-full-scope-" + Guid.NewGuid().ToString("N"));
|
|
|
|
var scope = AICameraGeneratorWindow.CreateGenerationControlScope(
|
|
false,
|
|
12,
|
|
unusedOutput,
|
|
"close",
|
|
"orbit_right",
|
|
"left_third",
|
|
9.5f,
|
|
2f,
|
|
1f);
|
|
|
|
Assert.That(scope.ShotSize, Is.EqualTo("auto"));
|
|
Assert.That(scope.Motion, Is.EqualTo("auto"));
|
|
Assert.That(scope.Composition, Is.EqualTo("auto"));
|
|
Assert.That(scope.DistanceMeters, Is.Zero);
|
|
Assert.That(scope.MotionIntensity, Is.EqualTo(1f));
|
|
Assert.That(scope.BodyFollowStrength, Is.EqualTo(0.18f));
|
|
Assert.That(scope.DirectivesPath, Is.Empty);
|
|
Assert.That(scope.UsesSelectedShotOverrides, Is.False);
|
|
Assert.That(Directory.Exists(unusedOutput), Is.False);
|
|
}
|
|
|
|
[Test]
|
|
public void SelectedShotRegenerationSerializesOnlyThatShotsOverrides()
|
|
{
|
|
var output = Path.Combine(
|
|
Path.GetTempPath(),
|
|
"cw-ai-selected-scope-" + Guid.NewGuid().ToString("N"));
|
|
try
|
|
{
|
|
var scope =
|
|
AICameraGeneratorWindow.CreateGenerationControlScope(
|
|
true,
|
|
7,
|
|
output,
|
|
"medium",
|
|
"truck_left",
|
|
"right_third",
|
|
6.25f,
|
|
1.4f,
|
|
0.08f);
|
|
|
|
Assert.That(scope.ShotSize, Is.EqualTo("auto"));
|
|
Assert.That(scope.Motion, Is.EqualTo("auto"));
|
|
Assert.That(scope.Composition, Is.EqualTo("auto"));
|
|
Assert.That(scope.DistanceMeters, Is.Zero);
|
|
Assert.That(scope.MotionIntensity, Is.EqualTo(1f));
|
|
Assert.That(scope.BodyFollowStrength, Is.EqualTo(0.18f));
|
|
Assert.That(scope.UsesSelectedShotOverrides, Is.True);
|
|
Assert.That(File.Exists(scope.DirectivesPath), Is.True);
|
|
|
|
var document = JsonUtility.FromJson<DirectiveDocument>(
|
|
File.ReadAllText(scope.DirectivesPath));
|
|
Assert.That(
|
|
document.schemaVersion,
|
|
Is.EqualTo("camera-directives-v1"));
|
|
Assert.That(document.shots, Has.Length.EqualTo(1));
|
|
Assert.That(document.shots[0].index, Is.EqualTo(7));
|
|
Assert.That(document.shots[0].shotSize, Is.EqualTo("medium"));
|
|
Assert.That(document.shots[0].motion, Is.EqualTo("truck_left"));
|
|
Assert.That(
|
|
document.shots[0].composition,
|
|
Is.EqualTo("right_third"));
|
|
Assert.That(document.shots[0].distanceMeters, Is.EqualTo(6.25f));
|
|
Assert.That(document.shots[0].motionIntensity, Is.EqualTo(1.4f));
|
|
Assert.That(document.shots[0].bodyFollowStrength, Is.EqualTo(0.08f));
|
|
}
|
|
finally
|
|
{
|
|
if (Directory.Exists(output))
|
|
{
|
|
Directory.Delete(output, true);
|
|
}
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
public void FullGenerationDoesNotEmitSelectedShotOneShotArguments()
|
|
{
|
|
Assert.That(
|
|
AICameraGeneratorWindow.BuildSelectedShotOneShotArguments(
|
|
-1,
|
|
string.Empty),
|
|
Is.Empty);
|
|
}
|
|
|
|
[Test]
|
|
public void SelectedShotOneShotArgumentsIncludeIndexAndBaseGeneration()
|
|
{
|
|
var baseGeneration = Path.Combine(
|
|
Path.GetTempPath(),
|
|
"cw ai base generation");
|
|
|
|
var arguments =
|
|
AICameraGeneratorWindow.BuildSelectedShotOneShotArguments(
|
|
7,
|
|
baseGeneration);
|
|
|
|
Assert.That(arguments, Does.Contain("--only-shot-index"));
|
|
Assert.That(arguments, Does.Contain("7"));
|
|
Assert.That(arguments, Does.Contain("--base-generation"));
|
|
Assert.That(
|
|
arguments,
|
|
Does.Contain(Path.GetFullPath(baseGeneration)));
|
|
}
|
|
|
|
[Test]
|
|
public void SelectedShotTimingUsesSeparateOneShotHistory()
|
|
{
|
|
var text =
|
|
AICameraGeneratorWindow.BuildSelectedShotGenerationTimingText(
|
|
4d,
|
|
10d,
|
|
AICameraGeneratorWindow.CacheObservation.Hit,
|
|
AICameraGeneratorWindow.CacheObservation.Miss);
|
|
|
|
Assert.That(text, Does.Contain("선택 카메라 1개만 계산 중"));
|
|
Assert.That(text, Does.Contain("최근 단일 샷 기준 총"));
|
|
Assert.That(text, Does.Contain("새 후보 풀 계산 중"));
|
|
}
|
|
|
|
[Test]
|
|
public void FailedImportPreservesPreviousGeneratedFolder()
|
|
{
|
|
var previous = Path.Combine(Path.GetTempPath(), "previous-base");
|
|
var failedCandidate = Path.Combine(
|
|
Path.GetTempPath(),
|
|
"failed-candidate");
|
|
|
|
Assert.That(
|
|
AICameraGeneratorWindow.GeneratedFolderAfterImport(
|
|
previous,
|
|
failedCandidate,
|
|
false),
|
|
Is.EqualTo(previous));
|
|
}
|
|
|
|
[Test]
|
|
public void SuccessfulImportAdvancesGeneratedFolder()
|
|
{
|
|
var candidate = Path.Combine(Path.GetTempPath(), "new-base");
|
|
|
|
Assert.That(
|
|
AICameraGeneratorWindow.GeneratedFolderAfterImport(
|
|
"previous-base",
|
|
candidate,
|
|
true),
|
|
Is.EqualTo(Path.GetFullPath(candidate)));
|
|
}
|
|
|
|
[Test]
|
|
public void HybridBaseContractAcceptsFullCompositeSchema()
|
|
{
|
|
Assert.DoesNotThrow(() =>
|
|
AICameraGeneratorWindow
|
|
.ValidateHybridBaseGenerationContractForTests(
|
|
HybridMetadataJson(),
|
|
HybridShotsJson()));
|
|
}
|
|
|
|
[Test]
|
|
public void HybridBaseContractRejectsNonHybridOutputBeforeWorkerStarts()
|
|
{
|
|
var exception = Assert.Throws<InvalidDataException>(() =>
|
|
AICameraGeneratorWindow
|
|
.ValidateHybridBaseGenerationContractForTests(
|
|
HybridMetadataJson(
|
|
generationMode: "unity_native_fast",
|
|
plannerMode: string.Empty,
|
|
targetAspectRatio: 0d),
|
|
HybridShotsJson("1.3")));
|
|
|
|
Assert.That(exception.Message, Does.Contain("Hybrid"));
|
|
}
|
|
|
|
[Test]
|
|
public void HybridBaseContractRequiresTargetAspectRatio()
|
|
{
|
|
var exception = Assert.Throws<InvalidDataException>(() =>
|
|
AICameraGeneratorWindow
|
|
.ValidateHybridBaseGenerationContractForTests(
|
|
HybridMetadataJson(targetAspectRatio: 0d),
|
|
HybridShotsJson()));
|
|
|
|
Assert.That(exception.Message, Does.Contain("targetAspectRatio"));
|
|
}
|
|
|
|
[Test]
|
|
public void HybridBaseContractRequiresShotsSchema12()
|
|
{
|
|
var exception = Assert.Throws<InvalidDataException>(() =>
|
|
AICameraGeneratorWindow
|
|
.ValidateHybridBaseGenerationContractForTests(
|
|
HybridMetadataJson(),
|
|
HybridShotsJson("1.3")));
|
|
|
|
Assert.That(exception.Message, Does.Contain("schema 1.2"));
|
|
}
|
|
|
|
[Test]
|
|
public void WindowProvenanceRecoveryPrefersCwAiRelativePathAfterMove()
|
|
{
|
|
var newRoot = Path.Combine(
|
|
Path.GetTempPath(),
|
|
"cw-ai-moved-" + Guid.NewGuid().ToString("N"));
|
|
var relative = Path.Combine(
|
|
"examples",
|
|
"UnityGenerated",
|
|
"camera_result");
|
|
var movedFolder = Path.Combine(newRoot, relative);
|
|
Directory.CreateDirectory(movedFolder);
|
|
try
|
|
{
|
|
Assert.That(
|
|
AICameraGeneratorWindow
|
|
.ResolveGeneratedFolderFromProvenance(
|
|
newRoot,
|
|
relative.Replace('\\', '/'),
|
|
@"D:\OldPc\CW-AI\examples\UnityGenerated\camera_result"),
|
|
Is.EqualTo(Path.GetFullPath(movedFolder)));
|
|
}
|
|
finally
|
|
{
|
|
Directory.Delete(newRoot, true);
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
public void WindowProvenanceRecoveryUsesAbsoluteFallback()
|
|
{
|
|
var fallback = Path.Combine(
|
|
Path.GetTempPath(),
|
|
"cw-ai-fallback-" + Guid.NewGuid().ToString("N"));
|
|
Directory.CreateDirectory(fallback);
|
|
try
|
|
{
|
|
Assert.That(
|
|
AICameraGeneratorWindow
|
|
.ResolveGeneratedFolderFromProvenance(
|
|
Path.Combine(fallback, "missing-root"),
|
|
"examples/UnityGenerated/missing",
|
|
fallback),
|
|
Is.EqualTo(Path.GetFullPath(fallback)));
|
|
}
|
|
finally
|
|
{
|
|
Directory.Delete(fallback, true);
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
public void WindowProvenanceRejectsMetadataChangedBehindStoredHash()
|
|
{
|
|
var generatedFolder = Path.Combine(
|
|
Path.GetTempPath(),
|
|
"cw-ai-provenance-hash-" + Guid.NewGuid().ToString("N"));
|
|
Directory.CreateDirectory(generatedFolder);
|
|
var metadataPath = Path.Combine(generatedFolder, "metadata.json");
|
|
try
|
|
{
|
|
File.WriteAllText(metadataPath, "{\"generation\":1}");
|
|
string expected;
|
|
using (var algorithm = SHA256.Create())
|
|
{
|
|
expected = BitConverter.ToString(
|
|
algorithm.ComputeHash(File.ReadAllBytes(metadataPath)))
|
|
.Replace("-", string.Empty)
|
|
.ToLowerInvariant();
|
|
}
|
|
|
|
Assert.DoesNotThrow(() =>
|
|
AICameraGeneratorWindow.ValidateProvenanceMetadataSha256(
|
|
generatedFolder,
|
|
expected));
|
|
File.WriteAllText(metadataPath, "{\"generation\":2}");
|
|
Assert.That(
|
|
() => AICameraGeneratorWindow
|
|
.ValidateProvenanceMetadataSha256(
|
|
generatedFolder,
|
|
expected),
|
|
Throws.TypeOf<InvalidDataException>()
|
|
.With.Message.Contains("does not match"));
|
|
}
|
|
finally
|
|
{
|
|
Directory.Delete(generatedFolder, true);
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
public void ColdStartHybridEstimateIsBroadAndMarkedInitial()
|
|
{
|
|
var estimate = AICameraGeneratorWindow.CalculateRuntimeEstimate(
|
|
204.48d,
|
|
0d,
|
|
0,
|
|
-1d,
|
|
0d);
|
|
|
|
Assert.That(estimate.IsAvailable, Is.True);
|
|
Assert.That(estimate.IsInitial, Is.True);
|
|
Assert.That(
|
|
estimate.MinimumTotalSeconds,
|
|
Is.EqualTo(79.4112d).Within(1e-4));
|
|
Assert.That(
|
|
estimate.MaximumTotalSeconds,
|
|
Is.EqualTo(148.23424d).Within(1e-4));
|
|
Assert.That(
|
|
estimate.MinimumRemainingSeconds,
|
|
Is.EqualTo(79.4112d).Within(1e-4));
|
|
Assert.That(
|
|
estimate.MaximumRemainingSeconds,
|
|
Is.EqualTo(148.23424d).Within(1e-4));
|
|
}
|
|
|
|
[Test]
|
|
public void HistoricalEstimateUsesObservedRateAndElapsedTime()
|
|
{
|
|
var estimate = AICameraGeneratorWindow.CalculateRuntimeEstimate(
|
|
200d,
|
|
20d,
|
|
4,
|
|
0.5d,
|
|
0.05d);
|
|
|
|
Assert.That(estimate.IsAvailable, Is.True);
|
|
Assert.That(estimate.IsInitial, Is.False);
|
|
Assert.That(estimate.MinimumTotalSeconds, Is.EqualTo(85d));
|
|
Assert.That(estimate.MaximumTotalSeconds, Is.EqualTo(115d));
|
|
Assert.That(estimate.MinimumRemainingSeconds, Is.EqualTo(65d));
|
|
Assert.That(estimate.MaximumRemainingSeconds, Is.EqualTo(95d));
|
|
}
|
|
|
|
[Test]
|
|
public void CandidateCacheHitReplacesGeneralHistoryWithShortTailBudget()
|
|
{
|
|
var estimate = AICameraGeneratorWindow.CalculateRuntimeEstimate(
|
|
200d,
|
|
10d,
|
|
4,
|
|
0.5d,
|
|
0.05d,
|
|
AICameraGeneratorWindow.CacheObservation.Hit,
|
|
AICameraGeneratorWindow.CacheObservation.Hit,
|
|
3,
|
|
false);
|
|
|
|
Assert.That(estimate.MinimumTotalSeconds, Is.EqualTo(13d));
|
|
Assert.That(estimate.MaximumTotalSeconds, Is.EqualTo(30d));
|
|
Assert.That(estimate.MinimumRemainingSeconds, Is.EqualTo(3d));
|
|
Assert.That(estimate.MaximumRemainingSeconds, Is.EqualTo(20d));
|
|
}
|
|
|
|
[Test]
|
|
public void CandidateCacheHitTailCountsDownFromObservationTime()
|
|
{
|
|
var initial = AICameraGeneratorWindow.CalculateRuntimeEstimate(
|
|
200d,
|
|
10d,
|
|
4,
|
|
0.5d,
|
|
0.05d,
|
|
AICameraGeneratorWindow.CacheObservation.Hit,
|
|
AICameraGeneratorWindow.CacheObservation.Hit,
|
|
3,
|
|
false,
|
|
10d);
|
|
var later = AICameraGeneratorWindow.CalculateRuntimeEstimate(
|
|
200d,
|
|
15d,
|
|
4,
|
|
0.5d,
|
|
0.05d,
|
|
AICameraGeneratorWindow.CacheObservation.Hit,
|
|
AICameraGeneratorWindow.CacheObservation.Hit,
|
|
3,
|
|
false,
|
|
10d);
|
|
|
|
Assert.That(initial.MaximumRemainingSeconds, Is.EqualTo(20d));
|
|
Assert.That(later.MaximumRemainingSeconds, Is.EqualTo(15d));
|
|
Assert.That(
|
|
later.MaximumRemainingSeconds,
|
|
Is.LessThan(initial.MaximumRemainingSeconds));
|
|
Assert.That(later.MaximumTotalSeconds, Is.EqualTo(30d));
|
|
}
|
|
|
|
[Test]
|
|
public void PreparationCacheMissAddsColdPreparationPenalty()
|
|
{
|
|
var estimate = AICameraGeneratorWindow.CalculateRuntimeEstimate(
|
|
200d,
|
|
20d,
|
|
4,
|
|
0.5d,
|
|
0.05d,
|
|
AICameraGeneratorWindow.CacheObservation.Miss,
|
|
AICameraGeneratorWindow.CacheObservation.Miss,
|
|
2,
|
|
false);
|
|
|
|
Assert.That(estimate.MinimumTotalSeconds, Is.EqualTo(103d));
|
|
Assert.That(estimate.MaximumTotalSeconds, Is.EqualTo(164d));
|
|
Assert.That(estimate.MinimumRemainingSeconds, Is.EqualTo(83d));
|
|
Assert.That(estimate.MaximumRemainingSeconds, Is.EqualTo(144d));
|
|
}
|
|
|
|
[Test]
|
|
public void ImportStageBoundsStaleRemainingEstimate()
|
|
{
|
|
var estimate = AICameraGeneratorWindow.CalculateRuntimeEstimate(
|
|
200d,
|
|
20d,
|
|
4,
|
|
0.5d,
|
|
0.05d,
|
|
AICameraGeneratorWindow.CacheObservation.Hit,
|
|
AICameraGeneratorWindow.CacheObservation.Miss,
|
|
4,
|
|
true);
|
|
|
|
Assert.That(estimate.MinimumRemainingSeconds, Is.Zero);
|
|
Assert.That(estimate.MaximumRemainingSeconds, Is.EqualTo(24d));
|
|
Assert.That(estimate.MaximumTotalSeconds, Is.EqualTo(44d));
|
|
}
|
|
|
|
[Test]
|
|
public void ImportStageTailCountsDownFromObservationTime()
|
|
{
|
|
var initial = AICameraGeneratorWindow.CalculateRuntimeEstimate(
|
|
200d,
|
|
20d,
|
|
4,
|
|
0.5d,
|
|
0.05d,
|
|
AICameraGeneratorWindow.CacheObservation.Hit,
|
|
AICameraGeneratorWindow.CacheObservation.Miss,
|
|
4,
|
|
true,
|
|
double.NaN,
|
|
20d);
|
|
var later = AICameraGeneratorWindow.CalculateRuntimeEstimate(
|
|
200d,
|
|
21d,
|
|
4,
|
|
0.5d,
|
|
0.05d,
|
|
AICameraGeneratorWindow.CacheObservation.Hit,
|
|
AICameraGeneratorWindow.CacheObservation.Miss,
|
|
4,
|
|
true,
|
|
double.NaN,
|
|
20d);
|
|
|
|
Assert.That(initial.MaximumRemainingSeconds, Is.EqualTo(24d));
|
|
Assert.That(later.MaximumRemainingSeconds, Is.EqualTo(23d));
|
|
Assert.That(
|
|
later.MaximumRemainingSeconds,
|
|
Is.LessThan(initial.MaximumRemainingSeconds));
|
|
Assert.That(initial.MaximumTotalSeconds, Is.EqualTo(44d));
|
|
Assert.That(later.MaximumTotalSeconds, Is.EqualTo(44d));
|
|
}
|
|
|
|
[Test]
|
|
public void InvalidDurationDoesNotClaimAnEstimate()
|
|
{
|
|
var estimate = AICameraGeneratorWindow.CalculateRuntimeEstimate(
|
|
0d,
|
|
10d,
|
|
2,
|
|
0.5d,
|
|
0.1d);
|
|
|
|
Assert.That(estimate.IsAvailable, Is.False);
|
|
Assert.That(estimate.MinimumTotalSeconds, Is.Zero);
|
|
Assert.That(estimate.MaximumTotalSeconds, Is.Zero);
|
|
Assert.That(estimate.MinimumRemainingSeconds, Is.Zero);
|
|
Assert.That(estimate.MaximumRemainingSeconds, Is.Zero);
|
|
}
|
|
|
|
[Test]
|
|
public void TimingTextShowsElapsedTotalRemainingAndCacheState()
|
|
{
|
|
var estimate = AICameraGeneratorWindow.CalculateRuntimeEstimate(
|
|
200d,
|
|
10d,
|
|
4,
|
|
0.5d,
|
|
0.05d,
|
|
AICameraGeneratorWindow.CacheObservation.Hit,
|
|
AICameraGeneratorWindow.CacheObservation.Hit,
|
|
3,
|
|
false);
|
|
|
|
var text = AICameraGeneratorWindow.BuildGenerationTimingText(
|
|
10d,
|
|
estimate,
|
|
AICameraGeneratorWindow.CacheObservation.Hit,
|
|
AICameraGeneratorWindow.CacheObservation.Hit);
|
|
|
|
Assert.That(text, Does.Contain("경과 10초"));
|
|
Assert.That(text, Does.Contain("최근 실측 예상 총"));
|
|
Assert.That(text, Does.Contain("남은 시간"));
|
|
Assert.That(text, Does.Contain("안전 후보 캐시 사용 중"));
|
|
}
|
|
|
|
[TestCase("Candidate cache hit: abc", "candidate", 1)]
|
|
[TestCase("[candidate-cache] miss: none", "candidate", 2)]
|
|
[TestCase(
|
|
"Candidate cache runtime validation failed; falling back",
|
|
"candidate",
|
|
2)]
|
|
[TestCase("Preparation cache invalid: sha mismatch", "preparation", 2)]
|
|
[TestCase("unrelated message", "candidate", 0)]
|
|
public void CacheLogObservationParsesStableSignals(
|
|
string message,
|
|
string cacheName,
|
|
int expected)
|
|
{
|
|
Assert.That(
|
|
AICameraGeneratorWindow.ParseCacheObservation(
|
|
message,
|
|
cacheName),
|
|
Is.EqualTo(
|
|
(AICameraGeneratorWindow.CacheObservation)expected));
|
|
}
|
|
|
|
[Test]
|
|
public void CandidateCacheModeDefaultsToAutoAndFreshPoolUsesOff()
|
|
{
|
|
Assert.That(
|
|
AICameraGeneratorWindow.CandidateCacheMode(false),
|
|
Is.EqualTo("auto"));
|
|
Assert.That(
|
|
AICameraGeneratorWindow.CandidateCacheMode(true),
|
|
Is.EqualTo("off"));
|
|
}
|
|
|
|
[Test]
|
|
public void AppliedSeedSummarySeparatesCurrentResultFromNextGeneration()
|
|
{
|
|
var summary = AICameraGeneratorWindow.BuildAppliedSeedSummary(
|
|
20489696,
|
|
20489697,
|
|
false,
|
|
-1);
|
|
|
|
Assert.That(
|
|
summary,
|
|
Is.EqualTo(
|
|
"현재 전체 생성 결과 Seed: 20489696\n" +
|
|
"다음 생성에 사용할 Seed: 20489697"));
|
|
}
|
|
|
|
[Test]
|
|
public void AppliedSeedSummaryMarksSelectedShotAsMixedResult()
|
|
{
|
|
var summary = AICameraGeneratorWindow.BuildAppliedSeedSummary(
|
|
20489696,
|
|
20489697,
|
|
true,
|
|
8);
|
|
|
|
Assert.That(summary, Does.Contain("카메라: 008"));
|
|
Assert.That(summary, Does.Contain("해당 카메라 Seed: 20489696"));
|
|
Assert.That(summary, Does.Contain("나머지 카메라는 기존 결과"));
|
|
}
|
|
|
|
[Test]
|
|
public void SeedAndReapplyLabelsDescribeDifferentActions()
|
|
{
|
|
Assert.That(
|
|
AICameraGeneratorWindow.NextGenerationSeedLabel,
|
|
Does.Contain("다음 생성"));
|
|
Assert.That(
|
|
AICameraGeneratorWindow.SelectedCameraRegenerationButtonLabel,
|
|
Does.Contain("안전 후보"));
|
|
Assert.That(
|
|
AICameraGeneratorWindow.ListedCameraRegenerationButtonLabel,
|
|
Does.Contain("안전 후보"));
|
|
Assert.That(
|
|
AICameraGeneratorWindow.ReapplyCurrentResultButtonLabel,
|
|
Does.Contain("Seed 사용 안 함"));
|
|
Assert.That(
|
|
AICameraGeneratorWindow.ReapplyCurrentResultWarning,
|
|
Does.Contain("새 후보를 생성하지 않으며"));
|
|
Assert.That(
|
|
AICameraGeneratorWindow.SeedCandidateExplanation,
|
|
Does.Contain("안전 후보 풀을 재사용"));
|
|
Assert.That(
|
|
AICameraGeneratorWindow.FreshCandidatePoolOptionLabel,
|
|
Does.Contain("후보 풀 캐시 무시하고 재계산"));
|
|
Assert.That(
|
|
AICameraGeneratorWindow.FullAutonomousGenerationButtonLabel,
|
|
Does.Contain("자율 생성"));
|
|
Assert.That(
|
|
AICameraGeneratorWindow.FullGenerationAutonomyExplanation,
|
|
Does.Contain("전체 생성에 적용되지 않습니다"));
|
|
Assert.That(
|
|
AICameraGeneratorWindow.SelectedShotOverrideExplanation,
|
|
Does.Contain("선택한 카메라를 다시 생성할 때만"));
|
|
}
|
|
}
|
|
}
|