fix: parse project rosters with legacy-compatible semantics

This commit is contained in:
codex
2026-08-06 15:14:08 +03:00
parent f460e87253
commit 2c578576d7
8 changed files with 118 additions and 25 deletions
+25 -7
View File
@@ -17,6 +17,7 @@ public partial class MainWindow : System.Windows.Window
private LauncherProduct? selected; private LauncherProduct? selected;
private string? projectPath; private string? projectPath;
private string? manualRosterPath; private string? manualRosterPath;
private string? selectedRosterSnapshotPath;
private string stage = "ProductSelection"; private string stage = "ProductSelection";
private string code = "PRODUCT_NOT_SELECTED"; private string code = "PRODUCT_NOT_SELECTED";
private string detail = ""; private string detail = "";
@@ -105,10 +106,10 @@ public partial class MainWindow : System.Windows.Window
pattern = localProfile is not null && selected.Id == "plant3d" ? localProfile.PatternName : product.Pattern; pattern = localProfile is not null && selected.Id == "plant3d" ? localProfile.PatternName : product.Pattern;
expectedRoster = manualRosterPath ?? LegacyProductSelections.AutomaticRoster(product, projectPath); expectedRoster = manualRosterPath ?? LegacyProductSelections.AutomaticRoster(product, projectPath);
Transition("AutomaticRosterPathResolved", "AUTOMATIC_ROSTER_PATH_RESOLVED", "Поиск списка плагинов", expectedRoster); Transition("AutomaticRosterPathResolved", "AUTOMATIC_ROSTER_PATH_RESOLVED", "Поиск списка плагинов", expectedRoster);
var plan = LegacyProductSelections.ReadRoster(product, projectPath, manualRosterPath, adapter); var plan = LegacyProductSelections.ReadRoster(product, projectPath, manualRosterPath, adapter, localProfile?.Root);
if (localProfile is not null && selected.Id == "plant3d" && !string.Equals(Path.GetFullPath(plan.RosterPath), Path.GetFullPath(localProfile.DownloadListPath), StringComparison.OrdinalIgnoreCase))
throw new InvalidOperationException("LOCAL_PARITY_ROSTER_PROFILE_MISMATCH");
orderedIds = string.Join(", ", plan.OrderedIds); orderedIds = string.Join(", ", plan.OrderedIds);
selectedRosterSnapshotPath = plan.SnapshotPath ?? plan.RosterPath;
if (plan.SnapshotSha256 is not null) detail = $"Roster snapshot SHA-256: {plan.SnapshotSha256}";
Plugins.ItemsSource = plan.OrderedIds; Plugins.ItemsSource = plan.OrderedIds;
Plugins.IsEnabled = false; Plugins.IsEnabled = false;
PluginStatus.Text = "Источник списка: " + (manualRosterPath is null ? "Автоматически" : "Выбран вручную"); PluginStatus.Text = "Источник списка: " + (manualRosterPath is null ? "Автоматически" : "Выбран вручную");
@@ -126,7 +127,14 @@ public partial class MainWindow : System.Windows.Window
Transition("PackageMappingFailed", "PACKAGE_MAPPING_NOT_FOUND", "Для выбранных плагинов отсутствуют пакеты 2026"); Transition("PackageMappingFailed", "PACKAGE_MAPPING_NOT_FOUND", "Для выбранных плагинов отсутствуют пакеты 2026");
return; return;
} }
ValidateLocalProfileDefinitions(plan); var unmapped = EvaluateLocalProfileDefinitions(plan);
if (unmapped.Count > 0)
{
mappingState = "Known legacy plugin, package 2026 absent: " + string.Join(", ", unmapped);
StartButton.IsEnabled = false;
Transition("PackageMappingFailed", "PACKAGE_MAPPING_NOT_FOUND", "Для части плагинов проекта пока отсутствуют пакеты версии 2026");
return;
}
mappingState = "Mapped"; mappingState = "Mapped";
StartButton.IsEnabled = true; StartButton.IsEnabled = true;
Transition("Ready", "READY", "Готово к запуску"); Transition("Ready", "READY", "Готово к запуску");
@@ -134,13 +142,22 @@ public partial class MainWindow : System.Windows.Window
catch (Exception ex) { ReportFailure(ex, "RosterResolutionFailed"); } catch (Exception ex) { ReportFailure(ex, "RosterResolutionFailed"); }
} }
private void ValidateLocalProfileDefinitions(LegacyPluginPlan plan) private IReadOnlyList<string> EvaluateLocalProfileDefinitions(LegacyPluginPlan plan)
{ {
var settings = adapter.ParseSettings(File.ReadAllText(localProfile!.SettingsPath)); var settings = adapter.ParseSettings(File.ReadAllText(localProfile!.SettingsPath));
var selectedPattern = settings.Patterns.SingleOrDefault(x => string.Equals(x.Name, localProfile.PatternName, StringComparison.Ordinal)); var selectedPattern = settings.Patterns.SingleOrDefault(x => string.Equals(x.Name, localProfile.PatternName, StringComparison.Ordinal));
if (selectedPattern is null) throw new InvalidOperationException("LOCAL_PARITY_PATTERN_NOT_FOUND"); if (selectedPattern is null) throw new InvalidOperationException("LOCAL_PARITY_PATTERN_NOT_FOUND");
var overlay = LegacyCompatibilityOverlayParser.Parse(File.ReadAllText(localProfile.OverlayPath)); var overlay = LegacyCompatibilityOverlayParser.Parse(File.ReadAllText(localProfile.OverlayPath));
adapter.Join(selectedPattern, new LegacyDownloadSelection(plan.OrderedIds), overlay); var missing = new List<string>();
foreach (var id in plan.OrderedIds.Distinct(StringComparer.OrdinalIgnoreCase))
{
var definitions = selectedPattern.Plugins.Where(x => string.Equals(x.Id, id, StringComparison.OrdinalIgnoreCase)).ToArray();
if (definitions.Length == 0) throw new InvalidOperationException("LEGACY_PLUGIN_ID_NOT_DEFINED");
if (definitions.Length > 1) throw new InvalidOperationException("AMBIGUOUS_LEGACY_PLUGIN_DEFINITION");
if (!definitions[0].IsAllowedOn(Environment.MachineName)) { missing.Add(id + " (machine restricted)"); continue; }
if (!overlay.Plugins.Any(x => string.Equals(x.LegacyId, id, StringComparison.OrdinalIgnoreCase))) missing.Add(id);
}
return missing;
} }
private async void Start_Click(object sender, System.Windows.RoutedEventArgs e) private async void Start_Click(object sender, System.Windows.RoutedEventArgs e)
@@ -151,7 +168,7 @@ public partial class MainWindow : System.Windows.Window
{ {
preparationState = "InProgress"; preparationState = "InProgress";
Transition("PackagePreparation", "PACKAGE_PREPARING", "Подготовка плагинов"); Transition("PackagePreparation", "PACKAGE_PREPARING", "Подготовка плагинов");
var profile = manualRosterPath is null ? localProfile : localProfile with { DownloadListPath = manualRosterPath }; var profile = localProfile with { DownloadListPath = selectedRosterSnapshotPath ?? localProfile.DownloadListPath };
var prepared = await localService.PrepareAsync(profile, CancellationToken.None); var prepared = await localService.PrepareAsync(profile, CancellationToken.None);
preparationState = "Succeeded"; preparationState = "Succeeded";
activationState = prepared.Activation.Outcome; activationState = prepared.Activation.Outcome;
@@ -174,6 +191,7 @@ public partial class MainWindow : System.Windows.Window
Plugins.IsEnabled = false; Plugins.IsEnabled = false;
StartButton.IsEnabled = false; StartButton.IsEnabled = false;
pattern = expectedRoster = orderedIds = detail = ""; pattern = expectedRoster = orderedIds = detail = "";
selectedRosterSnapshotPath = null;
mappingState = preparationState = activationState = bootstrapState = processState = "NotStarted"; mappingState = preparationState = activationState = bootstrapState = processState = "NotStarted";
processId = null; processId = null;
packageLoadMarker = "NotStarted"; packageLoadMarker = "NotStarted";
@@ -7,10 +7,11 @@ public static class LauncherOperationFailures
public static LauncherOperationFailure Classify(string code, string fallbackStage, bool processStartAttempted = false) => code switch public static LauncherOperationFailure Classify(string code, string fallbackStage, bool processStartAttempted = false) => code switch
{ {
"LEGACY_DOWNLOAD_LIST_NOT_FOUND" => new("RosterResolutionFailed", "AUTOMATIC_DOWNLOAD_LIST_NOT_FOUND", "Для выбранного проекта не найден список плагинов", false), "LEGACY_DOWNLOAD_LIST_NOT_FOUND" => new("RosterResolutionFailed", "AUTOMATIC_DOWNLOAD_LIST_NOT_FOUND", "Для выбранного проекта не найден список плагинов", false),
"LEGACY_DOWNLOAD_SELECTION_INVALID" => new("PluginPlanFailed", "DOWNLOAD_LIST_INVALID", "Список плагинов проекта содержит недопустимые или повторяющиеся записи", false), "DOWNLOAD_LIST_XML_MALFORMED" => new("PluginPlanFailed", "DOWNLOAD_LIST_XML_MALFORMED", "Некорректный download_list.xml", false),
"DOWNLOAD_LIST_ROOT_INVALID" => new("PluginPlanFailed", "DOWNLOAD_LIST_ROOT_INVALID", "download_list.xml имеет недопустимый корневой элемент", false),
"LEGACY_PLUGIN_ID_NOT_DEFINED" => new("PluginPlanFailed", "DOWNLOAD_LIST_ID_UNKNOWN", "В Settings.xml отсутствует описание плагина", false),
"AMBIGUOUS_LEGACY_PLUGIN_DEFINITION" => new("PluginPlanFailed", "DOWNLOAD_LIST_PLUGIN_DEFINITION_AMBIGUOUS", "Определение плагина в Settings.xml неоднозначно", false),
"LEGACY_PLUGIN_PRODUCT_MISMATCH" => new("PluginPlanFailed", "PLUGIN_PRODUCT_MISMATCH", "Список плагинов не совместим с выбранным продуктом", false), "LEGACY_PLUGIN_PRODUCT_MISMATCH" => new("PluginPlanFailed", "PLUGIN_PRODUCT_MISMATCH", "Список плагинов не совместим с выбранным продуктом", false),
"LEGACY_PLUGIN_ID_NOT_DEFINED" => new("PluginPlanFailed", "PLUGIN_DEFINITION_NOT_FOUND", "Для одного из плагинов отсутствует определение", false),
"AMBIGUOUS_LEGACY_PLUGIN_DEFINITION" => new("PluginPlanFailed", "PLUGIN_DEFINITION_AMBIGUOUS", "Определение плагина неоднозначно", false),
"LEGACY_MAPPING_NOT_FOUND" => new("PackageMappingFailed", "PACKAGE_MAPPING_NOT_FOUND", "Для выбранных плагинов отсутствуют пакеты 2026", false), "LEGACY_MAPPING_NOT_FOUND" => new("PackageMappingFailed", "PACKAGE_MAPPING_NOT_FOUND", "Для выбранных плагинов отсутствуют пакеты 2026", false),
"LOCAL_PARITY_ROSTER_PROFILE_MISMATCH" => new("RosterResolutionFailed", "OWNER_REVIEW_PROFILE_ROSTER_MISMATCH", "Контролируемая конфигурация не соответствует выбранному проекту", false), "LOCAL_PARITY_ROSTER_PROFILE_MISMATCH" => new("RosterResolutionFailed", "OWNER_REVIEW_PROFILE_ROSTER_MISMATCH", "Контролируемая конфигурация не соответствует выбранному проекту", false),
_ when code.Contains("PROCESS", StringComparison.Ordinal) || code.Contains("PLANT3D_2026_NOT_FOUND", StringComparison.Ordinal) => new("ProcessStartFailed", "PROCESS_START_FAILED", "Не удалось запустить Autodesk", true), _ when code.Contains("PROCESS", StringComparison.Ordinal) || code.Contains("PLANT3D_2026_NOT_FOUND", StringComparison.Ordinal) => new("ProcessStartFailed", "PROCESS_START_FAILED", "Не удалось запустить Autodesk", true),
@@ -10,7 +10,11 @@ public sealed record LegacyPluginDefinition(string Id, string Renewal, string Ca
} }
public sealed record LegacyProductPattern(string Name, string Filter, string Roster, IReadOnlyList<LegacyPluginDefinition> Plugins, IReadOnlyList<string>? Attributes = null, IReadOnlyList<string>? Sources = null, string? Target = null, string? Loader = null, string? ObjectPath = null); public sealed record LegacyProductPattern(string Name, string Filter, string Roster, IReadOnlyList<LegacyPluginDefinition> Plugins, IReadOnlyList<string>? Attributes = null, IReadOnlyList<string>? Sources = null, string? Target = null, string? Loader = null, string? ObjectPath = null);
public sealed record LegacyLauncherSettings(string Help, string CareMarker, IReadOnlyList<LegacyProductPattern> Patterns); public sealed record LegacyLauncherSettings(string Help, string CareMarker, IReadOnlyList<LegacyProductPattern> Patterns);
public sealed record LegacyDownloadSelection(IReadOnlyList<string> PluginIds); public sealed record LegacyDownloadListEntry(int ElementIndex, string RawId, string NormalizedId, int? FirstOccurrenceIndex, string SourceShape);
public sealed record LegacyDownloadSelection(IReadOnlyList<string> PluginIds, IReadOnlyList<LegacyDownloadListEntry>? Entries = null)
{
public IReadOnlyList<string> EffectivePluginIds => PluginIds.Distinct(StringComparer.OrdinalIgnoreCase).ToArray();
}
public sealed record LegacyCompatibilityMapping(string LegacyId, string PackageId, string Version, string Channel); public sealed record LegacyCompatibilityMapping(string LegacyId, string PackageId, string Version, string Channel);
public sealed record LegacyCompatibilityOverlay(string MappingId, string Product, int ProductYear, string Architecture, bool AllowOfflineFallback, IReadOnlyList<LegacyCompatibilityMapping> Plugins); public sealed record LegacyCompatibilityOverlay(string MappingId, string Product, int ProductYear, string Architecture, bool AllowOfflineFallback, IReadOnlyList<LegacyCompatibilityMapping> Plugins);
@@ -42,18 +46,39 @@ public sealed class LegacyConfigurationAdapter
public LegacyDownloadSelection ParseDownloadList(string xml) public LegacyDownloadSelection ParseDownloadList(string xml)
{ {
using var reader = XmlReader.Create(new StringReader(xml), XmlSettings); var root = XDocument.Load(reader).Root; try
if (root?.Name != "root") throw new InvalidOperationException("LEGACY_DOWNLOAD_LIST_INVALID"); {
var ids = root.Elements("id").Select(x => x.Value.Trim()).ToArray(); using var reader = XmlReader.Create(new StringReader(xml), XmlSettings);
if (ids.Length == 0 || ids.Any(string.IsNullOrWhiteSpace) || ids.GroupBy(x => x, StringComparer.OrdinalIgnoreCase).Any(x => x.Count() > 1)) var root = XDocument.Load(reader).Root;
throw new InvalidOperationException("LEGACY_DOWNLOAD_SELECTION_INVALID"); if (root?.Name != "root") throw new InvalidOperationException("DOWNLOAD_LIST_ROOT_INVALID");
return new(ids); // F2_Xml serialized only root/id and removed only exact empty strings. Real project files
// also contain source-grouped <area><id> entries; preserve their document order as the
// modern plan while retaining raw values and duplicate evidence.
var direct = root.Elements("id").Select(x => (Element: x, Shape: "Root"));
var grouped = root.Elements("area").SelectMany(area => area.Elements("id").Select(x => (Element: x, Shape: "Area")));
var entries = (direct.Any() ? direct : grouped).Select((entry, index) =>
{
var raw = entry.Element.Value;
var normalized = raw; // legacy F2_Xml did not Trim or case-normalize IDs.
return new LegacyDownloadListEntry(index, raw, normalized, null, entry.Shape);
}).ToArray();
var seen = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
var annotated = entries.Select(entry =>
{
if (entry.RawId == "") return entry;
if (seen.TryGetValue(entry.NormalizedId, out var first)) return entry with { FirstOccurrenceIndex = first };
seen[entry.NormalizedId] = entry.ElementIndex;
return entry;
}).ToArray();
return new(annotated.Where(x => x.RawId != "").Select(x => x.RawId).ToArray(), annotated);
}
catch (XmlException) { throw new InvalidOperationException("DOWNLOAD_LIST_XML_MALFORMED"); }
} }
public IReadOnlyList<PackageRequirement> Join(LegacyProductPattern pattern, LegacyDownloadSelection selection, LegacyCompatibilityOverlay overlay) public IReadOnlyList<PackageRequirement> Join(LegacyProductPattern pattern, LegacyDownloadSelection selection, LegacyCompatibilityOverlay overlay)
{ {
var result = new List<PackageRequirement>(); var result = new List<PackageRequirement>();
foreach (var id in selection.PluginIds) foreach (var id in selection.EffectivePluginIds)
{ {
var definition = pattern.Plugins.Where(x => string.Equals(x.Id, id, StringComparison.OrdinalIgnoreCase)).ToArray(); var definition = pattern.Plugins.Where(x => string.Equals(x.Id, id, StringComparison.OrdinalIgnoreCase)).ToArray();
if (definition.Length == 0) throw new InvalidOperationException("LEGACY_PLUGIN_ID_NOT_DEFINED"); if (definition.Length == 0) throw new InvalidOperationException("LEGACY_PLUGIN_ID_NOT_DEFINED");
@@ -59,3 +59,30 @@ public static class LegacyNetworkConfiguration
private static string Hash(string path) { using var stream = File.OpenRead(path); return Convert.ToHexString(SHA256.HashData(stream)); } private static string Hash(string path) { using var stream = File.OpenRead(path); return Convert.ToHexString(SHA256.HashData(stream)); }
private static string Fingerprint(string endpoint, string hash) => Convert.ToHexString(SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(endpoint + "|" + hash))); private static string Fingerprint(string endpoint, string hash) => Convert.ToHexString(SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(endpoint + "|" + hash)));
} }
public static class LegacyReadOnlySnapshots
{
public static LegacySettingsSnapshot Acquire(string source, string localRoot, string role)
{
if (!Path.IsPathFullyQualified(source) || !source.StartsWith(@"\\", StringComparison.Ordinal) || source.Contains('?') || source.Contains('*') || !File.Exists(source) || File.GetAttributes(source).HasFlag(FileAttributes.ReparsePoint)) throw new InvalidOperationException("READONLY_SNAPSHOT_SOURCE_INVALID");
var before = new FileInfo(source);
var beforeHash = Hash(source);
var directory = Path.Combine(Path.GetFullPath(localRoot), "configuration-snapshots", role);
Directory.CreateDirectory(directory);
var temporary = Path.Combine(directory, Guid.NewGuid().ToString("N") + ".tmp");
File.Copy(source, temporary, false);
var copiedHash = Hash(temporary);
var after = new FileInfo(source);
var afterHash = Hash(source);
if (before.Length != after.Length || before.LastWriteTimeUtc != after.LastWriteTimeUtc || beforeHash != afterHash || copiedHash != afterHash)
{
File.Delete(temporary);
throw new InvalidOperationException("READONLY_SNAPSHOT_SOURCE_CHANGED");
}
var final = Path.Combine(directory, copiedHash + ".xml");
if (File.Exists(final)) File.Delete(temporary); else File.Move(temporary, final);
return new(source, Convert.ToHexString(SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(role + "|" + source + "|" + copiedHash))), copiedHash, before.Length, new DateTimeOffset(before.LastWriteTimeUtc), DateTimeOffset.UtcNow, final, false);
}
private static string Hash(string path) { using var stream = File.OpenRead(path); return Convert.ToHexString(SHA256.HashData(stream)); }
}
@@ -1,7 +1,7 @@
namespace Launcher.Next.Application; namespace Launcher.Next.Application;
public sealed record LegacyProductSelection(string Id, string DisplayName, string Filter, string RequiredFileName, string Pattern, string[] SourceVerifiedLegacyIds, string[] UnmappedLegacyIds, bool SupportsPlantBootstrap); public sealed record LegacyProductSelection(string Id, string DisplayName, string Filter, string RequiredFileName, string Pattern, string[] SourceVerifiedLegacyIds, string[] UnmappedLegacyIds, bool SupportsPlantBootstrap);
public sealed record LegacyPluginPlan(string RosterPath, string Source, IReadOnlyList<string> OrderedIds, IReadOnlyList<string> UnmappedIds); public sealed record LegacyPluginPlan(string RosterPath, string Source, IReadOnlyList<string> OrderedIds, IReadOnlyList<string> UnmappedIds, string? SnapshotPath = null, string? SnapshotSha256 = null);
public static class LegacyProductSelections public static class LegacyProductSelections
{ {
@@ -26,12 +26,13 @@ public static class LegacyProductSelections
return Path.GetFullPath(Path.Combine(projectDirectory, "..", "_System", product.Pattern, "Applications", "download_list.xml")); return Path.GetFullPath(Path.Combine(projectDirectory, "..", "_System", product.Pattern, "Applications", "download_list.xml"));
} }
public static LegacyPluginPlan ReadRoster(LegacyProductSelection product, string selectedFile, string? manualXml, LegacyConfigurationAdapter parser) public static LegacyPluginPlan ReadRoster(LegacyProductSelection product, string selectedFile, string? manualXml, LegacyConfigurationAdapter parser, string? snapshotRoot = null)
{ {
var roster = manualXml is null ? AutomaticRoster(product, selectedFile) : Path.GetFullPath(manualXml); var roster = manualXml is null ? AutomaticRoster(product, selectedFile) : Path.GetFullPath(manualXml);
if (!File.Exists(roster) || File.GetAttributes(roster).HasFlag(FileAttributes.ReparsePoint) || !string.Equals(Path.GetFileName(roster), "download_list.xml", StringComparison.OrdinalIgnoreCase)) throw new InvalidOperationException("LEGACY_DOWNLOAD_LIST_NOT_FOUND"); if (!File.Exists(roster) || File.GetAttributes(roster).HasFlag(FileAttributes.ReparsePoint) || !string.Equals(Path.GetFileName(roster), "download_list.xml", StringComparison.OrdinalIgnoreCase)) throw new InvalidOperationException("LEGACY_DOWNLOAD_LIST_NOT_FOUND");
var ids = parser.ParseDownloadList(File.ReadAllText(roster)).PluginIds; var snapshot = roster.StartsWith(@"\\", StringComparison.Ordinal) && snapshotRoot is not null ? LegacyReadOnlySnapshots.Acquire(roster, snapshotRoot, "project-rosters") : null;
var ids = parser.ParseDownloadList(File.ReadAllText(snapshot?.LocalSnapshotPath ?? roster)).PluginIds;
if (product.SourceVerifiedLegacyIds.Length > 0 && ids.Any(id => !product.SourceVerifiedLegacyIds.Contains(id, StringComparer.OrdinalIgnoreCase))) throw new InvalidOperationException("LEGACY_PLUGIN_PRODUCT_MISMATCH"); if (product.SourceVerifiedLegacyIds.Length > 0 && ids.Any(id => !product.SourceVerifiedLegacyIds.Contains(id, StringComparer.OrdinalIgnoreCase))) throw new InvalidOperationException("LEGACY_PLUGIN_PRODUCT_MISMATCH");
return new(roster, manualXml is null ? "Automatic" : "Manual", ids, product.UnmappedLegacyIds.Intersect(ids, StringComparer.OrdinalIgnoreCase).ToArray()); return new(roster, manualXml is null ? "Automatic" : "Manual", ids, product.UnmappedLegacyIds.Intersect(ids, StringComparer.OrdinalIgnoreCase).ToArray(), snapshot?.LocalSnapshotPath, snapshot?.Sha256);
} }
} }
@@ -26,4 +26,24 @@ public sealed class LegacyNetworkSettingsParserTests
var plugin = new LegacyPluginDefinition("id", "", "catalog", "install", 0); var plugin = new LegacyPluginDefinition("id", "", "catalog", "install", 0);
Assert.True(plugin.IsAllowedOn(Environment.MachineName)); Assert.True(plugin.IsAllowedOn(Environment.MachineName));
} }
[Fact]
public void RealOwnerRosterShape_UsesAreaIdsInSourceOrderAndPreservesDuplicateEvidence()
{
const string roster = "<?xml version=\"1.0\" encoding=\"UTF-8\"?><root><!-- comment --><area><agname>1</agname><id>first</id><id> second </id><id>first</id><id></id></area><ignored><id>not-selected</id></ignored></root>";
var selection = new LegacyConfigurationAdapter().ParseDownloadList(roster);
Assert.Equal(["first", " second ", "first"], selection.PluginIds);
Assert.Equal(["first", " second "], selection.EffectivePluginIds);
Assert.Equal("Area", selection.Entries![0].SourceShape);
Assert.Equal(0, selection.Entries[2].FirstOccurrenceIndex);
Assert.Equal("", selection.Entries[3].RawId);
}
[Fact]
public void DownloadListErrors_AreSpecific()
{
var adapter = new LegacyConfigurationAdapter();
Assert.Equal("DOWNLOAD_LIST_ROOT_INVALID", Assert.Throws<InvalidOperationException>(() => adapter.ParseDownloadList("<other/>")).Message);
Assert.Equal("DOWNLOAD_LIST_XML_MALFORMED", Assert.Throws<InvalidOperationException>(() => adapter.ParseDownloadList("<root>")).Message);
}
} }
@@ -70,14 +70,15 @@ public sealed class LegacyProductSelectionTests
} }
[Fact] [Fact]
public void ProductIncompatibleAndDuplicateIds_AreBlocked() public void ProductIncompatibleIds_AreBlocked_AndDuplicateIdsRemainDiagnosticInput()
{ {
using var fixture = new SelectionFixture(); using var fixture = new SelectionFixture();
var project = fixture.ProjectFile("Project.xml"); var project = fixture.ProjectFile("Project.xml");
fixture.WriteAutomatic("Plant PnID_2019", "<root><id>unexpected</id></root>"); fixture.WriteAutomatic("Plant PnID_2019", "<root><id>unexpected</id></root>");
Assert.Equal("LEGACY_PLUGIN_PRODUCT_MISMATCH", Assert.Throws<InvalidOperationException>(() => LegacyProductSelections.ReadRoster(LegacyProductSelections.Pnid, project, null, new LegacyConfigurationAdapter())).Message); Assert.Equal("LEGACY_PLUGIN_PRODUCT_MISMATCH", Assert.Throws<InvalidOperationException>(() => LegacyProductSelections.ReadRoster(LegacyProductSelections.Pnid, project, null, new LegacyConfigurationAdapter())).Message);
fixture.WriteAutomatic("Plant PnID_2019", "<root><id>1111</id><id>1111</id></root>"); fixture.WriteAutomatic("Plant PnID_2019", "<root><id>1111</id><id>1111</id></root>");
Assert.Equal("LEGACY_DOWNLOAD_SELECTION_INVALID", Assert.Throws<InvalidOperationException>(() => LegacyProductSelections.ReadRoster(LegacyProductSelections.Pnid, project, null, new LegacyConfigurationAdapter())).Message); var duplicatePlan = LegacyProductSelections.ReadRoster(LegacyProductSelections.Pnid, project, null, new LegacyConfigurationAdapter());
Assert.Equal(["1111", "1111"], duplicatePlan.OrderedIds);
} }
[Fact] [Fact]
@@ -16,7 +16,7 @@ public sealed class OwnerReviewStateTests
} }
[Theory] [Theory]
[InlineData("LEGACY_DOWNLOAD_SELECTION_INVALID", "PluginPlanFailed", "DOWNLOAD_LIST_INVALID")] [InlineData("DOWNLOAD_LIST_XML_MALFORMED", "PluginPlanFailed", "DOWNLOAD_LIST_XML_MALFORMED")]
[InlineData("LEGACY_PLUGIN_PRODUCT_MISMATCH", "PluginPlanFailed", "PLUGIN_PRODUCT_MISMATCH")] [InlineData("LEGACY_PLUGIN_PRODUCT_MISMATCH", "PluginPlanFailed", "PLUGIN_PRODUCT_MISMATCH")]
[InlineData("LEGACY_MAPPING_NOT_FOUND", "PackageMappingFailed", "PACKAGE_MAPPING_NOT_FOUND")] [InlineData("LEGACY_MAPPING_NOT_FOUND", "PackageMappingFailed", "PACKAGE_MAPPING_NOT_FOUND")]
[InlineData("LOCAL_PARITY_ROSTER_PROFILE_MISMATCH", "RosterResolutionFailed", "OWNER_REVIEW_PROFILE_ROSTER_MISMATCH")] [InlineData("LOCAL_PARITY_ROSTER_PROFILE_MISMATCH", "RosterResolutionFailed", "OWNER_REVIEW_PROFILE_ROSTER_MISMATCH")]