Files
MBN_STOCK_WEBVIEW/tests/MBN_STOCK_WEBVIEW.LegacyApplication.Tests/LegacyRuntimeUiCatalogLoaderTests.cs

598 lines
22 KiB
C#

using System.Text;
using MMoneyCoderSharp.Data;
namespace MBN_STOCK_WEBVIEW.LegacyApplication.Tests;
public sealed class LegacyRuntimeUiCatalogLoaderTests
{
private static readonly Encoding Cp949 = CreateCp949Encoding();
[Fact]
public void ConfiguredLegacyBinDebugFiles_MatchEveryRuntimeProjection()
{
var legacyBinDebug = Environment.GetEnvironmentVariable(
"MBN_STOCK_N_LEGACY_BIN_DEBUG");
if (string.IsNullOrWhiteSpace(legacyBinDebug))
{
return;
}
var composition = LegacyRuntimeUiCatalogLoader.ResolveFromBaseDirectory(
legacyBinDebug);
Assert.Empty(composition.Warnings);
AssertRuntimeFixedFile(
legacyBinDebug,
"해외.ini",
LegacyFixedMarket.Overseas,
LegacyFixedActionCatalog.ExpectedOverseasCount,
composition.FixedCatalog);
AssertRuntimeFixedFile(
legacyBinDebug,
"환율.ini",
LegacyFixedMarket.Exchange,
LegacyFixedActionCatalog.ExpectedExchangeCount,
composition.FixedCatalog);
AssertRuntimeFixedFile(
legacyBinDebug,
"지수.ini",
LegacyFixedMarket.Index,
LegacyFixedActionCatalog.ExpectedIndexCount,
composition.FixedCatalog);
AssertRuntimeIndustryFile(
legacyBinDebug,
"업종_코스피.ini",
IndustryMarket.Kospi,
composition.IndustryLayout);
AssertRuntimeIndustryFile(
legacyBinDebug,
"업종_코스닥.ini",
IndustryMarket.Kosdaq,
composition.IndustryLayout);
AssertRuntimeComparisonFile(
legacyBinDebug,
composition.ComparisonLayout);
}
[Fact]
public void RuntimeFiles_ControlOnlyTrustedDisplayOrder()
{
using var directory = new TemporaryDirectory();
var fixedOrders = Enum.GetValues<LegacyFixedMarket>()
.ToDictionary(
market => market,
market => LegacyFixedActionCatalog.Default.GetActions(market)
.Reverse()
.ToArray());
var kospi = LegacyIndustryActionCatalog.GetActions(IndustryMarket.Kospi)
.Reverse()
.ToArray();
var kosdaq = LegacyIndustryActionCatalog.GetActions(IndustryMarket.Kosdaq)
.Reverse()
.ToArray();
var targets = LegacyComparisonWorkflowCatalog.FixedTargets
.Reverse()
.ToArray();
var directActions = LegacyComparisonWorkflowCatalog.Actions
.Where(action => action.Kind != LegacyComparisonActionKind.Return)
.Reverse()
.ToArray();
var returnActions = LegacyComparisonWorkflowCatalog.Actions
.Where(action => action.Kind == LegacyComparisonActionKind.Return)
.Reverse()
.ToArray();
WriteAllValidFiles(
directory.Path,
fixedOrders,
kospi,
kosdaq,
targets,
directActions,
returnActions,
returnSectionFirst: true);
var composition = LegacyRuntimeUiCatalogLoader.ResolveFromBaseDirectory(
directory.Path);
Assert.Empty(composition.Warnings);
foreach (var market in Enum.GetValues<LegacyFixedMarket>())
{
Assert.Equal(
FlattenForIni(fixedOrders[market]).Select(action => action.Id),
composition.FixedCatalog.GetActions(market).Select(action => action.Id));
}
Assert.Equal(
kospi.Select(action => action.ActionId),
composition.IndustryLayout.GetActions(IndustryMarket.Kospi)
.Select(action => action.ActionId));
Assert.Equal(
kosdaq.Select(action => action.ActionId),
composition.IndustryLayout.GetActions(IndustryMarket.Kosdaq)
.Select(action => action.ActionId));
Assert.Equal(
targets.Select(target => target.TargetId),
composition.ComparisonLayout.FixedTargets.Select(target => target.TargetId));
Assert.Equal(
returnActions.Select(action => action.ActionId)
.Concat(directActions.Select(action => action.ActionId)),
composition.ComparisonLayout.CreateActionViews(null, null)
.Select(action => action.ActionId));
var industryWorkflow = new LegacyIndustrySelectionWorkflow(
new EmptyIndustrySelectionService(),
actionLayout: composition.IndustryLayout);
Assert.Equal(
kospi.Select(action => action.ActionId),
industryWorkflow.ResetForTabActivation(IndustryMarket.Kospi)
.Actions.Select(action => action.ActionId));
var comparisonWorkflow = new LegacyComparisonWorkflowController(
new EmptyComparisonDataService(),
new EmptyComparisonPairStore(),
composition.ComparisonLayout);
Assert.Equal(
targets.Select(target => target.DisplayName),
comparisonWorkflow.Current.FixedTargets.Select(target => target.DisplayName));
Assert.Equal(
returnActions.Select(action => action.ActionId)
.Concat(directActions.Select(action => action.ActionId)),
comparisonWorkflow.Current.Actions.Select(action => action.ActionId));
}
[Fact]
public void ResolveFromResourceDirectory_ReadsTheDirectNativeSelectedDirectory()
{
using var directory = new TemporaryDirectory();
WriteAllValidFiles(directory.Path);
var resourceDirectory = System.IO.Path.Combine(directory.Path, "Res");
var composition = LegacyRuntimeUiCatalogLoader
.ResolveFromResourceDirectory(resourceDirectory);
Assert.Empty(composition.Warnings);
AssertRuntimeFixedFile(
directory.Path,
"해외.ini",
LegacyFixedMarket.Overseas,
LegacyFixedActionCatalog.ExpectedOverseasCount,
composition.FixedCatalog);
}
[Fact]
public void UnknownFixedRow_FallsBackWithoutRegisteringAnAction()
{
using var directory = new TemporaryDirectory();
WriteAllValidFiles(directory.Path);
WriteFixedFile(
directory.Path,
LegacyFixedMarket.Overseas,
LegacyFixedActionCatalog.Default.GetActions(LegacyFixedMarket.Overseas),
(action, index) => index == 0 ? "untrusted-new-action" : action.Label);
var composition = LegacyRuntimeUiCatalogLoader.ResolveFromBaseDirectory(
directory.Path);
var warning = Assert.Single(composition.Warnings);
Assert.Equal("해외.ini", warning.FileName);
Assert.Equal(LegacyRuntimeUiCatalogFailure.UnknownEntry, warning.Failure);
Assert.Equal(
LegacyFixedActionCatalog.Default.GetActions(LegacyFixedMarket.Overseas)
.Select(action => action.Id),
composition.FixedCatalog.GetActions(LegacyFixedMarket.Overseas)
.Select(action => action.Id));
Assert.DoesNotContain(
composition.FixedCatalog.Actions,
action => string.Equals(
action.Label,
"untrusted-new-action",
StringComparison.Ordinal));
}
[Fact]
public void ExcessDuplicateRow_FallsBackWithDuplicateWarning()
{
using var directory = new TemporaryDirectory();
WriteAllValidFiles(directory.Path);
WriteFixedFile(
directory.Path,
LegacyFixedMarket.Exchange,
LegacyFixedActionCatalog.Default.GetActions(LegacyFixedMarket.Exchange),
appendDuplicate: true);
var composition = LegacyRuntimeUiCatalogLoader.ResolveFromBaseDirectory(
directory.Path);
var warning = Assert.Single(composition.Warnings);
Assert.Equal("환율.ini", warning.FileName);
Assert.Equal(LegacyRuntimeUiCatalogFailure.DuplicateEntry, warning.Failure);
Assert.Equal(
LegacyFixedActionCatalog.Default.GetActions(LegacyFixedMarket.Exchange)
.Select(action => action.Id),
composition.FixedCatalog.GetActions(LegacyFixedMarket.Exchange)
.Select(action => action.Id));
}
[Fact]
public void InvalidCp949_FallsBackOnlyForThatFile()
{
using var directory = new TemporaryDirectory();
WriteAllValidFiles(directory.Path);
File.WriteAllBytes(
System.IO.Path.Combine(directory.Path, "Res", "지수.ini"),
[0x81]);
var composition = LegacyRuntimeUiCatalogLoader.ResolveFromBaseDirectory(
directory.Path);
var warning = Assert.Single(composition.Warnings);
Assert.Equal("지수.ini", warning.FileName);
Assert.Equal(LegacyRuntimeUiCatalogFailure.InvalidEncoding, warning.Failure);
Assert.Equal(
LegacyFixedActionCatalog.Default.GetActions(LegacyFixedMarket.Index)
.Select(action => action.Id),
composition.FixedCatalog.GetActions(LegacyFixedMarket.Index)
.Select(action => action.Id));
}
[Fact]
public void RowLimit_FailsClosedBeforeCatalogJoin()
{
using var directory = new TemporaryDirectory();
WriteAllValidFiles(directory.Path);
var section = LegacyFixedActionCatalog.Default
.GetActions(LegacyFixedMarket.Overseas)[0].Section;
WriteCp949(
System.IO.Path.Combine(directory.Path, "Res", "해외.ini"),
[$"[{section}]", .. Enumerable.Range(1, 51).Select(index => $"row-{index}")]);
var composition = LegacyRuntimeUiCatalogLoader.ResolveFromBaseDirectory(
directory.Path);
var warning = Assert.Single(composition.Warnings);
Assert.Equal("해외.ini", warning.FileName);
Assert.Equal(LegacyRuntimeUiCatalogFailure.TooManyRows, warning.Failure);
}
[Fact]
public void ComparisonDirectActionCannotBeReinterpretedAsAChildRow()
{
using var directory = new TemporaryDirectory();
WriteAllValidFiles(directory.Path);
var directActions = LegacyComparisonWorkflowCatalog.Actions
.Where(action => action.Kind != LegacyComparisonActionKind.Return)
.ToArray();
var returnActions = LegacyComparisonWorkflowCatalog.Actions
.Where(action => action.Kind == LegacyComparisonActionKind.Return)
.ToArray();
var lines = new List<string> { "[지수]" };
lines.AddRange(LegacyComparisonWorkflowCatalog.FixedTargets
.Select(target => target.DisplayName));
foreach (var action in directActions)
{
lines.Add($"[{action.Label}]");
}
lines.Add($"[{returnActions[0].Section}]");
lines.Add(directActions[0].Label);
lines.AddRange(returnActions.Select(action => action.Label));
WriteCp949(
System.IO.Path.Combine(directory.Path, "Res", "종목비교.ini"),
lines);
var composition = LegacyRuntimeUiCatalogLoader.ResolveFromBaseDirectory(
directory.Path);
var warning = Assert.Single(composition.Warnings);
Assert.Equal("종목비교.ini", warning.FileName);
Assert.Equal(LegacyRuntimeUiCatalogFailure.UnknownEntry, warning.Failure);
Assert.Equal(
LegacyComparisonWorkflowCatalog.Actions.Select(action => action.ActionId),
composition.ComparisonLayout.CreateActionViews(null, null)
.Select(action => action.ActionId));
}
private static void WriteAllValidFiles(
string baseDirectory,
IReadOnlyDictionary<LegacyFixedMarket, LegacyFixedAction[]>? fixedOrders = null,
IReadOnlyList<LegacyIndustryActionDefinition>? kospi = null,
IReadOnlyList<LegacyIndustryActionDefinition>? kosdaq = null,
IReadOnlyList<LegacyComparisonTarget>? targets = null,
IReadOnlyList<LegacyComparisonActionDefinition>? directActions = null,
IReadOnlyList<LegacyComparisonActionDefinition>? returnActions = null,
bool returnSectionFirst = false)
{
Directory.CreateDirectory(System.IO.Path.Combine(baseDirectory, "Res"));
foreach (var market in Enum.GetValues<LegacyFixedMarket>())
{
WriteFixedFile(
baseDirectory,
market,
fixedOrders is not null && fixedOrders.TryGetValue(market, out var ordered)
? ordered
: LegacyFixedActionCatalog.Default.GetActions(market));
}
WriteIndustryFile(
baseDirectory,
IndustryMarket.Kospi,
kospi ?? LegacyIndustryActionCatalog.GetActions(IndustryMarket.Kospi));
WriteIndustryFile(
baseDirectory,
IndustryMarket.Kosdaq,
kosdaq ?? LegacyIndustryActionCatalog.GetActions(IndustryMarket.Kosdaq));
WriteComparisonFile(
baseDirectory,
targets ?? LegacyComparisonWorkflowCatalog.FixedTargets,
directActions ?? LegacyComparisonWorkflowCatalog.Actions
.Where(action => action.Kind != LegacyComparisonActionKind.Return)
.ToArray(),
returnActions ?? LegacyComparisonWorkflowCatalog.Actions
.Where(action => action.Kind == LegacyComparisonActionKind.Return)
.ToArray(),
returnSectionFirst);
}
private static void WriteFixedFile(
string baseDirectory,
LegacyFixedMarket market,
IReadOnlyList<LegacyFixedAction> actions,
Func<LegacyFixedAction, int, string>? labelSelector = null,
bool appendDuplicate = false)
{
var lines = new List<string>();
var rowIndex = 0;
foreach (var section in actions.GroupBy(action => action.Section, StringComparer.Ordinal))
{
lines.Add($"[{section.Key}]");
foreach (var action in section)
{
lines.Add(labelSelector?.Invoke(action, rowIndex) ?? action.Label);
rowIndex++;
}
if (appendDuplicate)
{
lines.Add(section.First().Label);
appendDuplicate = false;
}
}
WriteCp949(
System.IO.Path.Combine(baseDirectory, "Res", FixedFileName(market)),
lines);
}
private static IEnumerable<LegacyFixedAction> FlattenForIni(
IEnumerable<LegacyFixedAction> actions) =>
actions.GroupBy(action => action.Section, StringComparer.Ordinal)
.SelectMany(group => group);
private static void WriteIndustryFile(
string baseDirectory,
IndustryMarket market,
IReadOnlyList<LegacyIndustryActionDefinition> actions)
{
var section = market == IndustryMarket.Kospi ? "코스피" : "코스닥";
WriteCp949(
System.IO.Path.Combine(baseDirectory, "Res", IndustryFileName(market)),
[$"[{section}]", .. actions.Select(action => action.Label)]);
}
private static void WriteComparisonFile(
string baseDirectory,
IReadOnlyList<LegacyComparisonTarget> targets,
IReadOnlyList<LegacyComparisonActionDefinition> directActions,
IReadOnlyList<LegacyComparisonActionDefinition> returnActions,
bool returnSectionFirst)
{
var lines = new List<string> { "[지수]" };
lines.AddRange(targets.Select(target => target.DisplayName));
void AddDirectSections()
{
foreach (var action in directActions)
{
lines.Add($"[{action.Label}]");
}
}
void AddReturnSection()
{
lines.Add($"[{returnActions[0].Section}]");
lines.AddRange(returnActions.Select(action => action.Label));
}
if (returnSectionFirst)
{
AddReturnSection();
AddDirectSections();
}
else
{
AddDirectSections();
AddReturnSection();
}
WriteCp949(
System.IO.Path.Combine(baseDirectory, "Res", "종목비교.ini"),
lines);
}
private static void WriteCp949(string path, IEnumerable<string> lines) =>
File.WriteAllText(path, string.Join("\r\n", lines) + "\r\n", Cp949);
private static void AssertRuntimeFixedFile(
string baseDirectory,
string fileName,
LegacyFixedMarket market,
int expectedCount,
LegacyFixedActionCatalog catalog)
{
var document = ReadTestIni(baseDirectory, fileName);
var actions = catalog.GetActions(market);
Assert.Equal(expectedCount, actions.Count);
Assert.Equal(
document.SelectMany(section => section.Rows),
actions.Select(action => action.Label));
Assert.Equal(
document.SelectMany(section => section.Rows.Select(_ => section.Name)),
actions.Select(action => action.Section));
}
private static void AssertRuntimeIndustryFile(
string baseDirectory,
string fileName,
IndustryMarket market,
LegacyIndustryActionLayout layout)
{
var section = Assert.Single(ReadTestIni(baseDirectory, fileName));
var actions = layout.GetActions(market);
Assert.Equal(LegacyIndustryActionCatalog.ActionsPerMarket, actions.Count);
Assert.Equal(section.Rows, actions.Select(action => action.Label));
Assert.All(actions, action => Assert.Equal(section.Name, action.Section));
}
private static void AssertRuntimeComparisonFile(
string baseDirectory,
LegacyComparisonUiLayout layout)
{
var document = ReadTestIni(baseDirectory, "종목비교.ini");
Assert.Equal(6, document.Count);
Assert.Equal(24, layout.FixedTargets.Count);
Assert.Equal(
document[0].Rows,
layout.FixedTargets.Select(target => target.DisplayName));
var expectedActions = document.Skip(1).SelectMany(section =>
section.Rows.Count == 0
? new[] { (Label: section.Name, Section: section.Name) }
: section.Rows.Select(row => (Label: row, Section: section.Name)));
var actualActions = layout.CreateActionViews(null, null);
Assert.Equal(9, actualActions.Count);
Assert.Equal(
expectedActions,
actualActions.Select(action => (action.Label, action.Section)));
}
private static IReadOnlyList<TestIniSection> ReadTestIni(
string baseDirectory,
string fileName)
{
var sections = new List<TestIniSection>();
List<string>? rows = null;
foreach (var source in File.ReadAllLines(
System.IO.Path.Combine(baseDirectory, "Res", fileName),
Cp949))
{
var line = source.Trim();
if (line.Length == 0)
{
continue;
}
if (line[0] == '[' && line[^1] == ']')
{
rows = [];
sections.Add(new TestIniSection(line[1..^1].Trim(), rows));
}
else
{
Assert.NotNull(rows);
rows.Add(line);
}
}
return sections;
}
private static string FixedFileName(LegacyFixedMarket market) => market switch
{
LegacyFixedMarket.Overseas => "해외.ini",
LegacyFixedMarket.Exchange => "환율.ini",
LegacyFixedMarket.Index => "지수.ini",
_ => throw new ArgumentOutOfRangeException(nameof(market), market, null)
};
private static string IndustryFileName(IndustryMarket market) => market switch
{
IndustryMarket.Kospi => "업종_코스피.ini",
IndustryMarket.Kosdaq => "업종_코스닥.ini",
_ => throw new ArgumentOutOfRangeException(nameof(market), market, null)
};
private static Encoding CreateCp949Encoding()
{
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
return Encoding.GetEncoding(
949,
EncoderFallback.ExceptionFallback,
DecoderFallback.ExceptionFallback);
}
private sealed class EmptyIndustrySelectionService : IIndustrySelectionService
{
public Task<IReadOnlyList<IndustrySelection>> GetAsync(
IndustryMarket market,
CancellationToken cancellationToken = default) =>
Task.FromResult<IReadOnlyList<IndustrySelection>>(
Array.Empty<IndustrySelection>());
}
private sealed class EmptyComparisonDataService : ILegacyComparisonDataService
{
public Task<IReadOnlyList<LegacyComparisonTarget>> SearchDomesticAsync(
string query,
int maximumResults,
CancellationToken cancellationToken = default) =>
Task.FromResult<IReadOnlyList<LegacyComparisonTarget>>(
Array.Empty<LegacyComparisonTarget>());
public Task<IReadOnlyList<LegacyComparisonTarget>> SearchWorldAsync(
string query,
int maximumResults,
CancellationToken cancellationToken = default) =>
Task.FromResult<IReadOnlyList<LegacyComparisonTarget>>(
Array.Empty<LegacyComparisonTarget>());
public Task<LegacyComparisonTarget?> ResolveStoredTargetAsync(
LegacyComparisonStoredTargetReference target,
CancellationToken cancellationToken = default) =>
Task.FromResult<LegacyComparisonTarget?>(null);
}
private sealed class EmptyComparisonPairStore : ILegacyComparisonPairStore
{
public Task<byte[]?> ReadCp949Async(
CancellationToken cancellationToken = default) =>
Task.FromResult<byte[]?>(null);
public Task WriteCp949Async(
IReadOnlyList<LegacyComparisonPairWriteRow> rows,
CancellationToken cancellationToken = default) =>
Task.CompletedTask;
}
private sealed class TemporaryDirectory : IDisposable
{
public TemporaryDirectory()
{
Path = System.IO.Path.Combine(
System.IO.Path.GetTempPath(),
$"MBN_STOCK_WEBVIEW_UI_INI_{Guid.NewGuid():N}");
Directory.CreateDirectory(Path);
}
public string Path { get; }
public void Dispose()
{
if (Directory.Exists(Path))
{
Directory.Delete(Path, recursive: true);
}
}
}
private sealed record TestIniSection(string Name, IReadOnlyList<string> Rows);
}