633 lines
29 KiB
C#
633 lines
29 KiB
C#
#if UNITY_EDITOR
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Net;
|
|
using System.Net.Http;
|
|
using System.Reflection;
|
|
using System.Runtime.ExceptionServices;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using game;
|
|
using game.Account;
|
|
using game.PlayerDataStorage;
|
|
using Newtonsoft.Json;
|
|
using UnityEditor;
|
|
using UnityEngine;
|
|
|
|
// Fixed B01 synthetic cases only. No global container replacement, permission UI or Play mode.
|
|
public static class KVPromptStateValidation
|
|
{
|
|
private static bool running;
|
|
private static object failedCleanup;
|
|
private static readonly BindingFlags PrivateInstance = BindingFlags.Instance | BindingFlags.NonPublic;
|
|
|
|
public static async Task<string> RunAsync(string caseId)
|
|
{
|
|
if ((caseId != "B01" && caseId != "CP3A" && caseId != "P6D") || EditorApplication.isPlayingOrWillChangePlaymode || running || failedCleanup != null)
|
|
{
|
|
throw new InvalidOperationException("B01 requires an idle Editor and completed prior cleanup.");
|
|
}
|
|
running = true;
|
|
var run = new FixedRun();
|
|
var staticField = typeof(RedPointManager).GetField("instance", BindingFlags.Static | BindingFlags.NonPublic);
|
|
object previous = null;
|
|
var isolated = false;
|
|
var errors = new List<Exception>();
|
|
string result = null;
|
|
try
|
|
{
|
|
previous = staticField.GetValue(null);
|
|
staticField.SetValue(null, new RedPointManager());
|
|
isolated = true;
|
|
result = caseId == "P6D" ? await run.ExecuteP6D() : caseId == "B01" ? await run.Execute() : await run.ExecuteCP3A();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
errors.Add(ex);
|
|
}
|
|
finally
|
|
{
|
|
try
|
|
{
|
|
await run.Close();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
failedCleanup = run;
|
|
errors.Add(ex);
|
|
}
|
|
try
|
|
{
|
|
if (isolated)
|
|
{
|
|
staticField.SetValue(null, previous);
|
|
if (!ReferenceEquals(previous, staticField.GetValue(null)))
|
|
{
|
|
throw new InvalidOperationException("Red point instance was not restored.");
|
|
}
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
failedCleanup = run;
|
|
errors.Add(ex);
|
|
}
|
|
finally
|
|
{
|
|
running = false;
|
|
}
|
|
}
|
|
if (errors.Count != 0)
|
|
{
|
|
throw new AggregateException(errors);
|
|
}
|
|
var report = Newtonsoft.Json.Linq.JObject.Parse(result);
|
|
report["restored"] = true;
|
|
report["remainingTestObjects"] = 0;
|
|
return report.ToString(Formatting.None);
|
|
}
|
|
|
|
sealed class Handler : HttpMessageHandler
|
|
{
|
|
readonly Func<HttpRequestMessage, HttpResponseMessage> send;
|
|
internal Handler(Func<HttpRequestMessage, HttpResponseMessage> send) { this.send = send; }
|
|
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage r, CancellationToken t) => Task.FromResult(send(r));
|
|
}
|
|
static HttpResponseMessage Json(object value) => new HttpResponseMessage(HttpStatusCode.OK)
|
|
{ Content = new StringContent(JsonConvert.SerializeObject(value)) };
|
|
|
|
sealed class FixedRun
|
|
{
|
|
private readonly List<FixedRun> children = new List<FixedRun>();
|
|
private readonly List<Action> restoreFaults = new List<Action>();
|
|
readonly Uri api = new Uri("http://b01-editor.invalid/");
|
|
readonly string cache = Path.GetFullPath(Path.Combine(Application.dataPath,
|
|
"../../CodexOutput/upserver/B3BatchMigration/B01Tests/unity-runs", Guid.NewGuid().ToString("N")));
|
|
readonly List<S101AccountSession> sessions = new List<S101AccountSession>();
|
|
readonly List<GameObject> objects = new List<GameObject>();
|
|
PlayerKVGameSessionBridge bridge;
|
|
long epoch = 1;
|
|
int assertions, requests;
|
|
void Check(bool value, string label) { assertions++; if (!value) throw new InvalidOperationException("B01: " + label); }
|
|
static object Call(object target, string method, params object[] args)
|
|
{
|
|
try { return target.GetType().GetMethod(method, PrivateInstance | BindingFlags.Public).Invoke(target, args); }
|
|
catch (TargetInvocationException ex) { ExceptionDispatchInfo.Capture(ex.InnerException).Throw(); throw; }
|
|
}
|
|
static void Field(object target, string name, object value) => target.GetType().GetField(name, PrivateInstance).SetValue(target, value);
|
|
static T Bound<T>(PlayerKVGameSessionHandle handle) => (T)typeof(T).GetConstructor(PrivateInstance,
|
|
null, new[] { typeof(PlayerKVGameSessionHandle) }, null).Invoke(new object[] { handle });
|
|
void Reject(Action action)
|
|
{
|
|
try { action(); }
|
|
catch (InvalidOperationException) { assertions++; return; }
|
|
throw new InvalidOperationException("B01: expected owner rejection");
|
|
}
|
|
async Task<PlayerKVGameSessionHandle> Open()
|
|
{
|
|
var session = new S101AccountSession(api, jar => new Handler(r =>
|
|
{
|
|
Check(r.RequestUri == new Uri(api, "account/Login"), "synthetic login only");
|
|
jar.Add(api, new Cookie("sid", "101"));
|
|
return Json(new { state = 0, internalId = 101, newCreate = false,
|
|
createTime = "2026-01-01T00:00:00Z", svrTime = "2026-01-01T00:00:00Z" });
|
|
}));
|
|
sessions.Add(session);
|
|
Check((await session.LoginAsync("101")).Success, "synthetic login");
|
|
var opened = await bridge.OpenAsync(session, cache, "101", api, epoch, e => e == epoch);
|
|
Check(opened.Success, "synthetic owner open");
|
|
return opened.Handle;
|
|
}
|
|
T Panel<T>() where T : Component
|
|
{
|
|
var go = new GameObject("B01-" + typeof(T).Name) { hideFlags = HideFlags.HideAndDontSave };
|
|
go.SetActive(false); objects.Add(go);
|
|
return go.AddComponent<T>();
|
|
}
|
|
internal async Task<string> Execute()
|
|
{
|
|
Func<CookieContainer, HttpMessageHandler> factory = jar => new Handler(r =>
|
|
{
|
|
Check(r.RequestUri == new Uri(api, "playerdata/Get"), "no write/no real transport");
|
|
requests++;
|
|
return Json(new { data = new Dictionary<string, string>() });
|
|
});
|
|
bridge = (PlayerKVGameSessionBridge)typeof(PlayerKVGameSessionBridge).GetConstructor(PrivateInstance,
|
|
null, new[] { typeof(Func<CookieContainer, HttpMessageHandler>), typeof(bool) }, null).Invoke(new object[] { factory, false });
|
|
var handle = await Open();
|
|
var settings = Bound<DefaultSettingService>(handle);
|
|
var club = Bound<ClubService>(handle);
|
|
Check(!settings.ShowNotice, "valid missing key");
|
|
settings.ShowNotice = true;
|
|
Check(settings.ShowNotice && requests == 1, "accepted local only");
|
|
var original = settings.GetType().GetProperty("LastPromptEdit", PrivateInstance).GetValue(settings);
|
|
settings.ShowNotice = true;
|
|
Check(ReferenceEquals(original, settings.GetType().GetProperty("LastPromptEdit", PrivateInstance).GetValue(settings)), "same value no duplicate");
|
|
// A fresh real ClubService retains its original initialized flag.
|
|
Check(club.IsOpenClubRed, "fresh real club retains original flag");
|
|
await bridge.CloseAsync();
|
|
Reject(() => settings.ShowNotice = false);
|
|
Reject(club.SetOpenClubRed);
|
|
Check(club.IsOpenClubRed, "stale real club preserves flag");
|
|
|
|
// Notification constructor validates the current owner. Create a fresh fixed instance, then revoke it.
|
|
epoch++;
|
|
var nextHandle = await Open();
|
|
var nextSettings = Bound<DefaultSettingService>(nextHandle);
|
|
var tables = new cfg.Tables(name => SimpleJSON.JSON.Parse(File.ReadAllText(
|
|
Path.Combine(Application.dataPath, "../../cfgData", name + ".json"))));
|
|
var notifications = (LocalNotificationService)typeof(LocalNotificationService).GetConstructor(PrivateInstance,
|
|
null, new[] { typeof(DefaultSettingService), typeof(cfg.Tables) }, null).Invoke(new object[] { nextSettings, tables });
|
|
var pendingInit = notifications.Init();
|
|
await bridge.CloseAsync();
|
|
try { await pendingInit; throw new InvalidOperationException("B01: stale init succeeded"); }
|
|
catch (InvalidOperationException ex) { Check(!ex.Message.Contains("stale init succeeded"), "stale Editor permission continuation rejected"); }
|
|
|
|
var grade = Panel<SettingNotificationsGradePopupPanel>();
|
|
Field(grade, "settings", nextSettings); Field(grade, "notifications", notifications);
|
|
var popup = Panel<SettingNotificationsPopupPanel>(); Field(popup, "notifications", notifications);
|
|
var settingPanel = Panel<SettingPopupPanel>(); Field(settingPanel, "settingService", nextSettings); Field(settingPanel, "notificationService", notifications);
|
|
var gift = Panel<GiftNoticePanel>(); Field(gift, "notificationService", notifications);
|
|
var social = Panel<FishingSocialPanel>(); Field(social, "_socialData", club);
|
|
var errors = new List<string>();
|
|
void Capture(string message, string stack, LogType type) { if (type == LogType.Error || type == LogType.Exception) errors.Add(message); }
|
|
Application.logMessageReceived += Capture;
|
|
try
|
|
{
|
|
Reject(() => Call(grade, "DestroyUI"));
|
|
Reject(() => Call(grade, "OnBtnOk"));
|
|
Reject(() => Call(popup, "OnBtnOk"));
|
|
Reject(() => Call(popup, "DestroyUI"));
|
|
Reject(() => Call(settingPanel, "OnToggleNotification", true));
|
|
Reject(() => Call(settingPanel, "OnToggleNotification", false));
|
|
Reject(() => Call(gift, "OnClickYES"));
|
|
Call(social, "Start");
|
|
Check(errors.Count == 1, "only unchanged social entry logs its owner rejection");
|
|
Check(errors.TrueForAll(e => e.Contains("ShowNotice owner=") || e.Contains("IsOpenClubRed owner=")), "no unexpected exception");
|
|
foreach (var go in objects) Check(go != null, "stale callback did not destroy another UI");
|
|
}
|
|
finally { Application.logMessageReceived -= Capture; }
|
|
Check(requests == 2, "read-only synthetic transport");
|
|
return JsonConvert.SerializeObject(new { success = true, assertions, requests, formalRequests = 0,
|
|
playing = EditorApplication.isPlaying, globalContainerChanged = false, caseId = "B01", cache });
|
|
}
|
|
internal async Task<string> ExecuteP6D()
|
|
{
|
|
foreach (var value in new[] { null, "0", "1", "" })
|
|
{
|
|
var fixture = new FixedRun();
|
|
children.Add(fixture);
|
|
await fixture.HealthyPrompt(value);
|
|
}
|
|
foreach (var club in new[] { false, true })
|
|
{
|
|
foreach (var stage in new[] { "BeforeWrite", "AfterReplace" })
|
|
{
|
|
var fixture = new FixedRun();
|
|
children.Add(fixture);
|
|
await fixture.FaultPrompt(club, stage);
|
|
}
|
|
var revoked = new FixedRun();
|
|
children.Add(revoked);
|
|
await revoked.RevokedDuringPrompt(club);
|
|
}
|
|
var callback = new FixedRun();
|
|
children.Add(callback);
|
|
await callback.RevokedDuringClubCallback();
|
|
foreach (var fixture in children)
|
|
{
|
|
assertions += fixture.assertions;
|
|
requests += fixture.requests;
|
|
}
|
|
return JsonConvert.SerializeObject(new { success = true, caseId = "P6D", assertions,
|
|
cases = children.Count, requests, formalRequests = 0, playing = EditorApplication.isPlaying,
|
|
globalContainerChanged = false });
|
|
}
|
|
|
|
private async Task<PlayerKVGameSessionHandle> OpenPrompt()
|
|
{
|
|
Func<CookieContainer, HttpMessageHandler> factory = jar => new Handler(r =>
|
|
{
|
|
Check(r.RequestUri == new Uri(api, "playerdata/Get"), "P6D synthetic Get only");
|
|
requests++;
|
|
return Json(new { data = new Dictionary<string, string>() });
|
|
});
|
|
bridge = (PlayerKVGameSessionBridge)typeof(PlayerKVGameSessionBridge).GetConstructor(PrivateInstance,
|
|
null, new[] { typeof(Func<CookieContainer, HttpMessageHandler>), typeof(bool) }, null).Invoke(new object[] { factory, false });
|
|
return await Open();
|
|
}
|
|
|
|
private static PlayerKVEditResult Edit(object service)
|
|
{
|
|
return (PlayerKVEditResult)service.GetType().GetProperty("LastPromptEdit", PrivateInstance).GetValue(service);
|
|
}
|
|
|
|
private static bool? Cached(DefaultSettingService settings)
|
|
{
|
|
return (bool?)typeof(DefaultSettingService).GetField("_showNotice", PrivateInstance).GetValue(settings);
|
|
}
|
|
|
|
private sealed class RedProbe : IUIRedPoint
|
|
{
|
|
private readonly Action<bool> callback;
|
|
internal RedProbe(Action<bool> callback)
|
|
{
|
|
this.callback = callback;
|
|
}
|
|
public void SetRedPointState(bool state)
|
|
{
|
|
callback(state);
|
|
}
|
|
}
|
|
|
|
private void ObserveRed(Action<bool> callback)
|
|
{
|
|
RedPointManager.Instance.RemoveRedPoint(RedPointName.Home_Club);
|
|
RedPointManager.Instance.AddRedPoint(RedPointName.Home_Club, new RedProbe(callback));
|
|
}
|
|
|
|
private static Delegate FaultDelegate<T>(Action<string> callback)
|
|
{
|
|
return new Action<T>(stage => callback(stage.ToString()));
|
|
}
|
|
|
|
private void InjectFault(PlayerKVGameSessionHandle handle, Action<string> callback)
|
|
{
|
|
var owner = handle.GetType().GetProperty("Owner", PrivateInstance).GetValue(handle);
|
|
var state = owner.GetType().GetField("state", PrivateInstance).GetValue(owner);
|
|
var store = state.GetType().GetField("store", PrivateInstance).GetValue(state);
|
|
var field = store.GetType().GetField("fault", PrivateInstance);
|
|
var previous = field.GetValue(store);
|
|
var method = typeof(FixedRun).GetMethod("FaultDelegate", BindingFlags.Static | BindingFlags.NonPublic)
|
|
.MakeGenericMethod(field.FieldType.GetGenericArguments()[0]);
|
|
var fault = method.Invoke(null, new object[] { callback });
|
|
restoreFaults.Add(() => field.SetValue(store, previous));
|
|
field.SetValue(store, fault);
|
|
Check(ReferenceEquals(field.GetValue(store), fault), "actual cache fault installed");
|
|
}
|
|
|
|
private async Task HealthyPrompt(string initial)
|
|
{
|
|
var handle = await OpenPrompt();
|
|
if (initial != null)
|
|
{
|
|
Check(handle.Update("ShowNotice", initial).Accepted, "seed initial value");
|
|
}
|
|
var settings = Bound<DefaultSettingService>(handle);
|
|
var expected = initial == "1";
|
|
Check(settings.ShowNotice == expected, "first read follows original string comparison");
|
|
Check(handle.Update("ShowNotice", expected ? "0" : "1").Accepted, "other consumer updates actual cache");
|
|
Check(settings.ShowNotice == expected, "first-read nullable cache survives other consumer writes");
|
|
var fresh = Bound<DefaultSettingService>(handle);
|
|
var target = !expected;
|
|
var observedWrites = 0;
|
|
InjectFault(handle, stage =>
|
|
{
|
|
if (stage == "BeforeWrite")
|
|
{
|
|
observedWrites++;
|
|
Check(Cached(fresh) == target, "nullable cache assigned before actual disk write");
|
|
}
|
|
});
|
|
fresh.ShowNotice = target;
|
|
Check(Edit(fresh)?.Accepted == true && observedWrites == 1, "setter before getter writes even when stored value matches");
|
|
var receipt = Edit(fresh);
|
|
fresh.ShowNotice = target;
|
|
Check(ReferenceEquals(receipt, Edit(fresh)) && observedWrites == 1, "same cached value does not write");
|
|
target = !target;
|
|
fresh.ShowNotice = target;
|
|
Check(observedWrites == 2 && !ReferenceEquals(receipt, Edit(fresh)), "changed cached value writes");
|
|
RestoreFaults();
|
|
|
|
Check(handle.Update("IsOpenClubRed", "0").Accepted, "seed present club value");
|
|
var club = Bound<ClubService>(handle);
|
|
var calls = 0;
|
|
ObserveRed(red =>
|
|
{
|
|
calls++;
|
|
Check(!red && !club.IsOpenClubRed, "flag clears before red callback");
|
|
Check(handle.ReadLocalSnapshot().Data["IsOpenClubRed"] == (calls == 1 ? "0" : "1"), "red callback precedes write");
|
|
});
|
|
club.SetOpenClubRed();
|
|
Check(calls == 1 && Edit(club)?.Accepted == true && handle.ReadLocalSnapshot().Data["IsOpenClubRed"] == "1", "present key does not replace original flag gate");
|
|
var clubReceipt = Edit(club);
|
|
club.SetOpenClubRed();
|
|
Check(calls == 1 && ReferenceEquals(clubReceipt, Edit(club)), "false flag skips red and write");
|
|
club.IsOpenClubRed = true;
|
|
club.SetOpenClubRed();
|
|
Check(calls == 2 && !ReferenceEquals(clubReceipt, Edit(club)), "true flag writes existing value");
|
|
|
|
// A diagnostic result is not a second owner state. Do not unlock a real failed owner.
|
|
var rejected = handle.Update(null, "1");
|
|
Check(!rejected.Accepted, "actual rejected diagnostic");
|
|
fresh.GetType().GetProperty("LastPromptEdit", PrivateInstance).SetValue(fresh, rejected);
|
|
club.GetType().GetProperty("LastPromptEdit", PrivateInstance).SetValue(club, rejected);
|
|
Check(fresh.ShowNotice == target, "healthy owner ignores old rejected diagnostic");
|
|
club.SetOpenClubRed();
|
|
Check(calls == 2, "healthy false club ignores diagnostic without business effects");
|
|
await bridge.CloseAsync();
|
|
Reject(() =>
|
|
{
|
|
_ = fresh.ShowNotice;
|
|
});
|
|
Reject(() => fresh.ShowNotice = target);
|
|
Reject(club.SetOpenClubRed);
|
|
Check(calls == 2 && Cached(fresh) == target && !club.IsOpenClubRed, "stale cache-hit entries cause no effects");
|
|
}
|
|
|
|
private async Task FaultPrompt(bool useClub, string failureStage)
|
|
{
|
|
var handle = await OpenPrompt();
|
|
var settings = Bound<DefaultSettingService>(handle);
|
|
var club = Bound<ClubService>(handle);
|
|
var redCalls = 0;
|
|
ObserveRed(red =>
|
|
{
|
|
redCalls++;
|
|
Check(!red && !club.IsOpenClubRed, "failed write still follows original red order");
|
|
});
|
|
var faults = 0;
|
|
InjectFault(handle, stage =>
|
|
{
|
|
if (stage == failureStage)
|
|
{
|
|
faults++;
|
|
Check(useClub ? !club.IsOpenClubRed && redCalls == 1 : Cached(settings) == true,
|
|
"business state precedes actual storage failure");
|
|
throw new IOException("P6D synthetic " + failureStage);
|
|
}
|
|
});
|
|
Reject(() =>
|
|
{
|
|
if (useClub)
|
|
{
|
|
club.SetOpenClubRed();
|
|
}
|
|
else
|
|
{
|
|
settings.ShowNotice = true;
|
|
}
|
|
});
|
|
var edit = Edit(useClub ? (object)club : settings);
|
|
Check(faults == 1 && edit != null && !edit.Accepted, "actual rejected write preserved");
|
|
Check(edit.StateResult.Error == PlayerKVChangeError.StorageError &&
|
|
edit.StateResult.LocalCommitMayHaveSucceeded == (failureStage == "AfterReplace"), "actual uncertainty preserved");
|
|
Check(handle.Status == PlayerKVSyncStatus.StorageError, "actual storage terminal state retained");
|
|
Reject(() =>
|
|
{
|
|
_ = settings.ShowNotice;
|
|
});
|
|
Reject(() => settings.ShowNotice = true);
|
|
Reject(club.SetOpenClubRed);
|
|
Check(faults == 1 && redCalls == (useClub ? 1 : 0), "actual unreadable owner blocks repeat effects");
|
|
Check(useClub ? !club.IsOpenClubRed : Cached(settings) == true, "no added rollback");
|
|
}
|
|
|
|
private async Task RevokedDuringPrompt(bool useClub)
|
|
{
|
|
var handle = await OpenPrompt();
|
|
var settings = Bound<DefaultSettingService>(handle);
|
|
var club = Bound<ClubService>(handle);
|
|
var redCalls = 0;
|
|
ObserveRed(red =>
|
|
{
|
|
redCalls++;
|
|
});
|
|
InjectFault(handle, stage =>
|
|
{
|
|
if (stage == "AfterReplace")
|
|
{
|
|
epoch++;
|
|
}
|
|
});
|
|
Reject(() =>
|
|
{
|
|
if (useClub)
|
|
{
|
|
club.SetOpenClubRed();
|
|
}
|
|
else
|
|
{
|
|
settings.ShowNotice = true;
|
|
}
|
|
});
|
|
Check(Edit(useClub ? (object)club : settings)?.Accepted == true, "completed disk write not disguised as unaccepted");
|
|
Check(useClub ? !club.IsOpenClubRed && redCalls == 1 : Cached(settings) == true, "revocation does not roll back original front effects");
|
|
Check(!handle.ReadLocalSnapshot().Success && requests == 1, "revoked old owner has no replay or transport write");
|
|
}
|
|
|
|
private async Task RevokedDuringClubCallback()
|
|
{
|
|
var handle = await OpenPrompt();
|
|
var club = Bound<ClubService>(handle);
|
|
var redCalls = 0;
|
|
ObserveRed(red =>
|
|
{
|
|
redCalls++;
|
|
epoch++;
|
|
});
|
|
var writes = 0;
|
|
InjectFault(handle, stage =>
|
|
{
|
|
writes++;
|
|
});
|
|
Reject(club.SetOpenClubRed);
|
|
Check(!club.IsOpenClubRed && redCalls == 1, "callback revocation preserves front effects");
|
|
Check(Edit(club)?.Accepted == false && writes == 0 && requests == 1, "callback revocation refuses old write without replay");
|
|
}
|
|
internal async Task<string> ExecuteCP3A()
|
|
{
|
|
Func<CookieContainer, HttpMessageHandler> factory = jar => new Handler(r =>
|
|
{
|
|
Check(r.RequestUri == new Uri(api, "playerdata/Get"), "CP3A synthetic Get only");
|
|
requests++;
|
|
return Json(new { data = new Dictionary<string, string>() });
|
|
});
|
|
bridge = (PlayerKVGameSessionBridge)typeof(PlayerKVGameSessionBridge).GetConstructor(PrivateInstance,
|
|
null, new[] { typeof(Func<CookieContainer, HttpMessageHandler>), typeof(bool) }, null).Invoke(new object[] { factory, false });
|
|
var tables = new cfg.Tables(name => SimpleJSON.JSON.Parse(File.ReadAllText(
|
|
Path.Combine(Application.dataPath, "../../cfgData", name + ".json"))));
|
|
var settingCtor = typeof(DefaultSettingService).GetConstructor(PrivateInstance,
|
|
null, new[] { typeof(bool) }, null);
|
|
var clubCtor = typeof(ClubService).GetConstructor(PrivateInstance,
|
|
null, new[] { typeof(bool) }, null);
|
|
var notificationCtor = typeof(LocalNotificationService).GetConstructor(PrivateInstance,
|
|
null, new[] { typeof(cfg.Tables) }, null);
|
|
Check(settingCtor != null && clubCtor != null && notificationCtor != null,
|
|
"formal service constructors exist");
|
|
var settings = (DefaultSettingService)settingCtor.Invoke(new object[] { true });
|
|
var club = (ClubService)clubCtor.Invoke(new object[] { true });
|
|
var notifications = (LocalNotificationService)notificationCtor.Invoke(new object[] { tables });
|
|
var pendingNotifications = (LocalNotificationService)notificationCtor.Invoke(new object[] { tables });
|
|
var pendingInit = pendingNotifications.Init();
|
|
Reject(() => Call(pendingNotifications, "PreflightFormalPromptSettings", settings));
|
|
await pendingInit;
|
|
await notifications.Init();
|
|
Reject(() => { _ = settings.ShowNotice; });
|
|
Reject(() => settings.ShowNotice = true);
|
|
Reject(club.SetOpenClubRed);
|
|
var handle = await Open();
|
|
Call(settings, "BindFormalPromptOwner", handle);
|
|
Call(club, "BindFormalPromptOwner", handle);
|
|
Call(notifications, "BindFormalPromptSettings", settings);
|
|
Call(settings, "BindFormalPromptOwner", handle);
|
|
Call(club, "BindFormalPromptOwner", handle);
|
|
Call(notifications, "BindFormalPromptSettings", settings);
|
|
Check(!settings.ShowNotice, "bound missing ShowNotice");
|
|
settings.ShowNotice = true;
|
|
club.SetOpenClubRed();
|
|
Check(settings.ShowNotice && !club.IsOpenClubRed, "bound prompt effects");
|
|
Check(requests == 1, "one synthetic KV Get and no transport write");
|
|
await bridge.CloseAsync();
|
|
Reject(() => { _ = settings.ShowNotice; });
|
|
Reject(club.SetOpenClubRed);
|
|
epoch++;
|
|
var nextHandle = await Open();
|
|
Reject(() => Call(settings, "BindFormalPromptOwner", nextHandle));
|
|
Reject(() => Call(club, "BindFormalPromptOwner", nextHandle));
|
|
var partialSetting = (DefaultSettingService)settingCtor.Invoke(new object[] { true });
|
|
var partialClub = (ClubService)clubCtor.Invoke(new object[] { true });
|
|
Call(partialSetting, "BindFormalPromptOwner", nextHandle);
|
|
await bridge.CloseAsync();
|
|
Reject(() => Call(partialClub, "BindFormalPromptOwner", nextHandle));
|
|
Reject(() => { _ = partialSetting.ShowNotice; });
|
|
epoch++;
|
|
var thirdHandle = await Open();
|
|
Reject(() => Call(partialSetting, "BindFormalPromptOwner", thirdHandle));
|
|
Check(requests == 3, "same owner binds and partial failure add no transport requests");
|
|
return JsonConvert.SerializeObject(new { success = true, caseId = "CP3A", assertions,
|
|
requests, formalRequests = 0, globalContainerChanged = false });
|
|
}
|
|
internal async Task Close()
|
|
{
|
|
var errors = new List<Exception>();
|
|
foreach (var child in children)
|
|
{
|
|
try
|
|
{
|
|
await child.Close();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
errors.Add(ex);
|
|
}
|
|
}
|
|
try
|
|
{
|
|
RestoreFaults();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
errors.Add(ex);
|
|
}
|
|
try
|
|
{
|
|
if (bridge != null)
|
|
{
|
|
await bridge.CloseAsync();
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
errors.Add(ex);
|
|
}
|
|
foreach (var session in sessions)
|
|
{
|
|
try
|
|
{
|
|
session.Dispose();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
errors.Add(ex);
|
|
}
|
|
}
|
|
foreach (var go in objects)
|
|
{
|
|
try
|
|
{
|
|
if (go != null)
|
|
{
|
|
UnityEngine.Object.DestroyImmediate(go);
|
|
}
|
|
if (go != null)
|
|
{
|
|
throw new InvalidOperationException("Test object survived cleanup.");
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
errors.Add(ex);
|
|
}
|
|
}
|
|
if (errors.Count != 0)
|
|
{
|
|
throw new AggregateException(errors);
|
|
}
|
|
}
|
|
|
|
private void RestoreFaults()
|
|
{
|
|
var errors = new List<Exception>();
|
|
foreach (var restore in restoreFaults)
|
|
{
|
|
try
|
|
{
|
|
restore();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
errors.Add(ex);
|
|
}
|
|
}
|
|
if (errors.Count != 0)
|
|
{
|
|
throw new AggregateException(errors);
|
|
}
|
|
restoreFaults.Clear();
|
|
}
|
|
}
|
|
}
|
|
#endif
|