// Copyright 2026 TesterArmy.
// Modified by Dario Kondratiuk.
// SPDX-License-Identifier: Apache-2.0
using System.Globalization;
using Xunit;
using Xunit.Sdk;
using Xunit.v3;
namespace E2E.XUnit.V3;
///
/// Base class for an xUnit v3 test that drives the app. starts one
/// engine session. commits the replay cache from the xUnit result: a
/// pass or a skip records verified acts, a failure evicts the unverified ones it recorded or
/// replayed, and a cancelled test leaves the cache alone. xUnit has no retry, so every test is a
/// first attempt and can replay. A check in a derived DisposeAsync after a failure verifies nothing.
/// Each Expect.Soft failure is kept on the session. The test body runs on, and the test
/// fails when it is disposed, with every soft failure in one message.
/// Settings come from .
///
[LooksBeforeTeardown]
public abstract class E2ETest : E2EFixture, IAsyncLifetime
{
// The attribute that looks before the teardown gets the test, not the instance.
private static readonly System.Collections.Concurrent.ConcurrentDictionary Running = new(StringComparer.Ordinal);
///
/// The test's title: its cache identity, and the Test: line every prompt's context opens with.
/// The default is the class and method name, with every theory
/// argument in full, so each test and theory row keeps its own replay. The xUnit display name
/// is not used, because xUnit shortens long arguments in it and a display setting can change it.
///
protected virtual string CacheTitle(ITest test)
{
ArgumentNullException.ThrowIfNull(test);
var name = test.TestCase.TestClassName + "." + test.TestCase.TestMethodName;
return test is IXunitTest { TestMethodArguments.Length: > 0 } row
? name + "(" + string.Join(", ", row.TestMethodArguments.Select(argument => FormatArgument(argument, row.TestMethodArguments.Length > 1))) + ")"
: name;
}
// A lazy or self-containing collection must not hang the title.
private const int MaxTitleDepth = 4;
private const int MaxTitleItems = 1000;
// A simple argument keeps its plain text. A null, a collection, or a string that could pass for
// another row's argument list is written so that no two rows share a title. A comma only matters
// when the row has several arguments, where ("a, b") and ("a", "b") would read alike.
private static string FormatArgument(object? argument, bool quoteComma, int depth = 0)
{
return argument switch
{
null => "null",
string text => text.Contains('"', StringComparison.Ordinal)
|| text.Contains('\\', StringComparison.Ordinal)
|| text.StartsWith('[')
|| text == "null"
|| (quoteComma && text.Contains(',', StringComparison.Ordinal))
? "\"" + text.Replace("\\", "\\\\", StringComparison.Ordinal).Replace("\"", "\\\"", StringComparison.Ordinal) + "\""
: text,
System.Collections.IEnumerable items when depth < MaxTitleDepth => "[" + string.Join(", ", items.Cast