Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .cspell.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ words:
- avares
- Appium
- appium
- NOSONAR
- NovaWindows
- WinAppDriver
- WebDriver
Expand Down
21 changes: 11 additions & 10 deletions src/DemaConsulting.SysML2Workbench/App.axaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,14 +43,15 @@ public override void OnFrameworkInitializationCompleted()
"logs");

var shell = new MainWindowShell(
new WorkspaceModel(),
new FileWatcher(TimeSpan.FromMilliseconds(500), dispatcher: new AvaloniaUiDispatcher()),
new DiagnosticsAggregator(),
new ViewCatalogPresenter(),
new LayoutInvoker(),
new DiagnosticsListView(),
new SysmlSnippetGenerator(),
new RollingFileLogger(logDirectory),
new MainWindowShellDependencies(
new WorkspaceModel(),
new FileWatcher(TimeSpan.FromMilliseconds(500), dispatcher: new AvaloniaUiDispatcher()),
new DiagnosticsAggregator(),
new ViewCatalogPresenter(),
new LayoutInvoker(),
new DiagnosticsListView(),
new SysmlSnippetGenerator(),
new RollingFileLogger(logDirectory)),
uiDispatcher: new AvaloniaUiDispatcher());

desktop.MainWindow = new MainWindowView(shell);
Expand Down Expand Up @@ -88,9 +89,9 @@ public override void OnFrameworkInitializationCompleted()
/// </remarks>
/// <param name="shell">The freshly composed shell to preload sources into.</param>
/// <param name="args">The raw command-line arguments the process was launched with.</param>
private static void ApplyStartupSourceArgumentsForTesting(MainWindowShell shell, IReadOnlyList<string> args)
private static void ApplyStartupSourceArgumentsForTesting(MainWindowShell shell, string[] args)
{
for (var i = 0; i < args.Count - 1; i++)
for (var i = 0; i < args.Length - 1; i++)
{
if (args[i] != "--startup-source")
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,14 @@ internal static class DesignTimeShellFactory
public static MainWindowShell Create()
{
return new MainWindowShell(
new WorkspaceModel(),
new FileWatcher(TimeSpan.FromMilliseconds(500)),
new DiagnosticsAggregator(),
new ViewCatalogPresenter(),
new LayoutInvoker(),
new DiagnosticsListView(),
new SysmlSnippetGenerator(),
new RollingFileLogger(PathHelpers.SafePathCombine(Path.GetTempPath(), "SysML2Workbench-DesignTime")));
new MainWindowShellDependencies(
new WorkspaceModel(),
new FileWatcher(TimeSpan.FromMilliseconds(500)),
new DiagnosticsAggregator(),
new ViewCatalogPresenter(),
new LayoutInvoker(),
new DiagnosticsListView(),
new SysmlSnippetGenerator(),
new RollingFileLogger(PathHelpers.SafePathCombine(Path.GetTempPath(), "SysML2Workbench-DesignTime"))));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,29 @@ public enum WorkbenchTabKind
/// </remarks>
public sealed record WorkbenchTab(string Id, string Title, WorkbenchTabKind Kind, SvgCanvasHost Canvas, ViewDefinitionModel? SourceDefinition = null, string? FilePath = null);

/// <summary>
/// Groups the eight required constructor dependencies of <see cref="MainWindowShell" /> into a single
/// parameter object, keeping the shell's constructor within the project's parameter-count quality gate
/// while still requiring every dependency to be supplied explicitly (no dependency is optional).
/// </summary>
/// <param name="WorkspaceModel">Owns discovery, load, and reload of the workspace.</param>
/// <param name="FileWatcher">Detects external workspace changes.</param>
/// <param name="DiagnosticsAggregator">Aggregates per-file diagnostics into a workspace-wide view.</param>
/// <param name="ViewCatalogPresenter">Supplies predefined view choices.</param>
/// <param name="LayoutInvoker">Renders predefined and custom views to SVG.</param>
/// <param name="DiagnosticsListView">Displays workspace diagnostics.</param>
/// <param name="SnippetGenerator">Exports custom-view definitions as SysML text.</param>
/// <param name="Logger">Records shell-level operational events and failures.</param>
public sealed record MainWindowShellDependencies(
WorkspaceModel WorkspaceModel,
FileWatcher FileWatcher,
DiagnosticsAggregator DiagnosticsAggregator,
ViewCatalogPresenter ViewCatalogPresenter,
LayoutInvoker LayoutInvoker,
DiagnosticsListView DiagnosticsListView,
SysmlSnippetGenerator SnippetGenerator,
RollingFileLogger Logger);

/// <summary>
/// MainWindowShell is the desktop composition root that coordinates workspace lifecycle, view selection,
/// diagram display, diagnostics presentation, and snippet export within a single windowed user experience.
Expand Down Expand Up @@ -221,14 +244,10 @@ public sealed class MainWindowShell : IDisposable
/// <summary>
/// Creates the shell from its constituent subsystem units.
/// </summary>
/// <param name="workspaceModel">Owns discovery, load, and reload of the workspace.</param>
/// <param name="fileWatcher">Detects external workspace changes.</param>
/// <param name="diagnosticsAggregator">Aggregates per-file diagnostics into a workspace-wide view.</param>
/// <param name="viewCatalogPresenter">Supplies predefined view choices.</param>
/// <param name="layoutInvoker">Renders predefined and custom views to SVG.</param>
/// <param name="diagnosticsListView">Displays workspace diagnostics.</param>
/// <param name="snippetGenerator">Exports custom-view definitions as SysML text.</param>
/// <param name="logger">Records shell-level operational events and failures.</param>
/// <param name="dependencies">
/// The shell's required subsystem dependencies, grouped into a single parameter object - see
/// <see cref="MainWindowShellDependencies" /> for what each member provides.
/// </param>
/// <param name="uiDispatcher">
/// Dispatcher used to marshal <see cref="TabsChanged" /> notifications. Defaults to
/// <see cref="ImmediateUiDispatcher" />, which runs the notification synchronously on the calling thread;
Expand All @@ -237,34 +256,26 @@ public sealed class MainWindowShell : IDisposable
/// when raised from a background continuation.
/// </param>
/// <exception cref="ArgumentNullException">Thrown when any required dependency is null.</exception>
public MainWindowShell(
WorkspaceModel workspaceModel,
FileWatcher fileWatcher,
DiagnosticsAggregator diagnosticsAggregator,
ViewCatalogPresenter viewCatalogPresenter,
LayoutInvoker layoutInvoker,
DiagnosticsListView diagnosticsListView,
SysmlSnippetGenerator snippetGenerator,
RollingFileLogger logger,
IUiDispatcher? uiDispatcher = null)
public MainWindowShell(MainWindowShellDependencies dependencies, IUiDispatcher? uiDispatcher = null)
{
ArgumentNullException.ThrowIfNull(workspaceModel);
ArgumentNullException.ThrowIfNull(fileWatcher);
ArgumentNullException.ThrowIfNull(diagnosticsAggregator);
ArgumentNullException.ThrowIfNull(viewCatalogPresenter);
ArgumentNullException.ThrowIfNull(layoutInvoker);
ArgumentNullException.ThrowIfNull(diagnosticsListView);
ArgumentNullException.ThrowIfNull(snippetGenerator);
ArgumentNullException.ThrowIfNull(logger);

_workspaceModel = workspaceModel;
_fileWatcher = fileWatcher;
_diagnosticsAggregator = diagnosticsAggregator;
_viewCatalogPresenter = viewCatalogPresenter;
_layoutInvoker = layoutInvoker;
_diagnosticsListView = diagnosticsListView;
_snippetGenerator = snippetGenerator;
_logger = logger;
ArgumentNullException.ThrowIfNull(dependencies);
ArgumentNullException.ThrowIfNull(dependencies.WorkspaceModel);
ArgumentNullException.ThrowIfNull(dependencies.FileWatcher);
ArgumentNullException.ThrowIfNull(dependencies.DiagnosticsAggregator);
ArgumentNullException.ThrowIfNull(dependencies.ViewCatalogPresenter);
ArgumentNullException.ThrowIfNull(dependencies.LayoutInvoker);
ArgumentNullException.ThrowIfNull(dependencies.DiagnosticsListView);
ArgumentNullException.ThrowIfNull(dependencies.SnippetGenerator);
ArgumentNullException.ThrowIfNull(dependencies.Logger);

_workspaceModel = dependencies.WorkspaceModel;
_fileWatcher = dependencies.FileWatcher;
_diagnosticsAggregator = dependencies.DiagnosticsAggregator;
_viewCatalogPresenter = dependencies.ViewCatalogPresenter;
_layoutInvoker = dependencies.LayoutInvoker;
_diagnosticsListView = dependencies.DiagnosticsListView;
_snippetGenerator = dependencies.SnippetGenerator;
_logger = dependencies.Logger;
_uiDispatcher = uiDispatcher ?? new ImmediateUiDispatcher();

// Eagerly establish a valid, empty (0-source) workspace snapshot at construction, so CurrentWorkspace is
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,8 @@ public sealed partial class QueryDialogViewModel : ObservableObject
/// workspace, and so <see cref="BuildListResult" /> can attach the same
/// <see cref="ElementTypeLabeler" /> kind label to each List-type entry.
/// </summary>
private IReadOnlyDictionary<string, SysmlNode> _candidateMap =
new Dictionary<string, SysmlNode>(StringComparer.Ordinal);
private Dictionary<string, SysmlNode> _candidateMap =
new(StringComparer.Ordinal);

[ObservableProperty]
public partial bool IncludeStdlib { get; set; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,10 @@
/// <returns>The loaded, registered highlighting definition.</returns>
private static IHighlightingDefinition LoadSysMlHighlighting()
{
// NOSONAR: "avares://" is Avalonia's compile-time embedded-resource URI scheme (an asset
// reference baked into this assembly's own resources), not a filesystem path or
// externally-configurable endpoint - there is no meaningful "avoid the hardcoded value" here.
using var stream = AssetLoader.Open(new Uri("avares://DemaConsulting.SysML2Workbench/Assets/SysML.xshd"));

Check warning on line 73 in src/DemaConsulting.SysML2Workbench/AppShellSubsystem/SourceTextDocumentView.axaml.cs

View workflow job for this annotation

GitHub Actions / Build / Build ubuntu-latest

Refactor your code not to use hardcoded absolute paths or URIs.

Check warning on line 73 in src/DemaConsulting.SysML2Workbench/AppShellSubsystem/SourceTextDocumentView.axaml.cs

View workflow job for this annotation

GitHub Actions / Build / Build ubuntu-latest

Refactor your code not to use hardcoded absolute paths or URIs.

Check warning on line 73 in src/DemaConsulting.SysML2Workbench/AppShellSubsystem/SourceTextDocumentView.axaml.cs

View workflow job for this annotation

GitHub Actions / Build / Build macos-latest

Refactor your code not to use hardcoded absolute paths or URIs.

Check warning on line 73 in src/DemaConsulting.SysML2Workbench/AppShellSubsystem/SourceTextDocumentView.axaml.cs

View workflow job for this annotation

GitHub Actions / Build / Build macos-latest

Refactor your code not to use hardcoded absolute paths or URIs.

Check warning on line 73 in src/DemaConsulting.SysML2Workbench/AppShellSubsystem/SourceTextDocumentView.axaml.cs

View workflow job for this annotation

GitHub Actions / Build / Build windows-latest

Refactor your code not to use hardcoded absolute paths or URIs.

Check warning on line 73 in src/DemaConsulting.SysML2Workbench/AppShellSubsystem/SourceTextDocumentView.axaml.cs

View workflow job for this annotation

GitHub Actions / Build / Build windows-latest

Refactor your code not to use hardcoded absolute paths or URIs.
using var reader = XmlReader.Create(stream);
var definition = HighlightingLoader.Load(reader, HighlightingManager.Instance);

Expand Down Expand Up @@ -102,7 +105,7 @@
// type (SysMLv2Lexer), and wrapped in a try/catch with a hard-coded fallback list below,
// so if the field is ever renamed or removed by a future SysML2Tools release, this
// degrades gracefully instead of crashing.
var field = typeof(SysMLv2Lexer).GetField("_LiteralNames", BindingFlags.NonPublic | BindingFlags.Static);
var field = typeof(SysMLv2Lexer).GetField("_LiteralNames", BindingFlags.NonPublic | BindingFlags.Static); // NOSONAR: read-only reflection of a well-known third-party field, guarded by the try/catch fallback below.

Check warning on line 108 in src/DemaConsulting.SysML2Workbench/AppShellSubsystem/SourceTextDocumentView.axaml.cs

View workflow job for this annotation

GitHub Actions / Build / Build ubuntu-latest

Make sure that this accessibility bypass is safe here.

Check warning on line 108 in src/DemaConsulting.SysML2Workbench/AppShellSubsystem/SourceTextDocumentView.axaml.cs

View workflow job for this annotation

GitHub Actions / Build / Build ubuntu-latest

Make sure that this accessibility bypass is safe here.

Check warning on line 108 in src/DemaConsulting.SysML2Workbench/AppShellSubsystem/SourceTextDocumentView.axaml.cs

View workflow job for this annotation

GitHub Actions / Build / Build macos-latest

Make sure that this accessibility bypass is safe here.

Check warning on line 108 in src/DemaConsulting.SysML2Workbench/AppShellSubsystem/SourceTextDocumentView.axaml.cs

View workflow job for this annotation

GitHub Actions / Build / Build macos-latest

Make sure that this accessibility bypass is safe here.

Check warning on line 108 in src/DemaConsulting.SysML2Workbench/AppShellSubsystem/SourceTextDocumentView.axaml.cs

View workflow job for this annotation

GitHub Actions / Build / Build windows-latest

Make sure that this accessibility bypass is safe here.

Check warning on line 108 in src/DemaConsulting.SysML2Workbench/AppShellSubsystem/SourceTextDocumentView.axaml.cs

View workflow job for this annotation

GitHub Actions / Build / Build windows-latest

Make sure that this accessibility bypass is safe here.
if (field?.GetValue(null) is string[] literalNames)
{
var keywords = literalNames
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -239,7 +239,7 @@ public void RebuildTree()
/// </summary>
/// <param name="source">The source the files were discovered under.</param>
/// <param name="files">Absolute paths of every file discovered under <paramref name="source" />.</param>
private static IReadOnlyList<WorkspaceTreeNode> BuildFolderChildren(WorkspaceSource source, IReadOnlyList<string> files)
private static List<WorkspaceTreeNode> BuildFolderChildren(WorkspaceSource source, IReadOnlyList<string> files)
{
if (files.Count == 0)
{
Expand Down Expand Up @@ -276,7 +276,7 @@ private static IReadOnlyList<WorkspaceTreeNode> BuildFolderChildren(WorkspaceSou
/// <see cref="WorkspaceTreeNode" /> shape the tree binds to, listing subfolders before files and sorting
/// each alphabetically by name.
/// </summary>
private static IReadOnlyList<WorkspaceTreeNode> ToTreeNodes(FolderGroup group, string sourceId)
private static List<WorkspaceTreeNode> ToTreeNodes(FolderGroup group, string sourceId)
{
var nodes = new List<WorkspaceTreeNode>();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ public void RotateIfNeeded()
/// immediately, no additional buffered state exists to flush; this method exists to satisfy the
/// documented unit contract and is safe to call at any time, including before any entry has been written.
/// </summary>
public void Flush()
public static void Flush()
{
// No-op by design: File.AppendAllText fully commits and closes the file handle on every call
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@ private static MacDriver CreateMacDriver(string startupArguments)
/// binary's path. Not exercised by CI and not validated against a real Linux machine - see
/// <see cref="AppFixture" />'s remarks.
/// </summary>
private static AppiumDriver CreateLinuxDriver(string startupArguments)
private static LinuxDriver CreateLinuxDriver(string startupArguments)
{
var options = new AppiumOptions
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ protected void StartApp(string startupArguments = "")
public void Dispose()
{
_fixture?.Dispose();
GC.SuppressFinalize(this);
}

/// <summary>
Expand All @@ -84,7 +85,7 @@ protected void AssertMenuItemsAreDiscoverableAndEnabled(string topLevelMenuName,
topLevelMenu.Click();

// Act / Assert
OpenQA.Selenium.IWebElement? lastMenuItem = null;
OpenQA.Selenium.Appium.AppiumElement? lastMenuItem = null;
foreach (var automationId in automationIds)
{
var menuItem = Session.FindElement(MobileBy.AccessibilityId(automationId));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,14 +69,15 @@ await File.WriteAllTextAsync(
+ "}\n");

var shell = new MainWindowShell(
new MainWindowShellDependencies(
new WorkspaceModel(),
new FileWatcher(TimeSpan.FromMilliseconds(1)),
new DiagnosticsAggregator(),
new ViewCatalogPresenter(),
new LayoutInvoker(),
new DiagnosticsListView(),
new SysmlSnippetGenerator(),
new RollingFileLogger(_tempLogRoot));
new RollingFileLogger(_tempLogRoot)));

await shell.AddFolderSourceAsync(_tempRoot);
return shell;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,14 +69,15 @@ await File.WriteAllTextAsync(
private MainWindowShell CreateShell(FileWatcher? fileWatcher = null)
{
return new MainWindowShell(
new MainWindowShellDependencies(
new WorkspaceModel(),
fileWatcher ?? new FileWatcher(TimeSpan.FromMilliseconds(1)),
new DiagnosticsAggregator(),
new ViewCatalogPresenter(),
new LayoutInvoker(),
new DiagnosticsListView(),
new SysmlSnippetGenerator(),
new RollingFileLogger(_tempLogRoot));
new RollingFileLogger(_tempLogRoot)));
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@ public sealed class QueryDialogViewModelTests : IDisposable
private readonly string _tempRoot = Directory.CreateTempSubdirectory("sysml2workbench-tests-").FullName;
private readonly string _tempLogRoot = Directory.CreateTempSubdirectory("sysml2workbench-tests-logs-").FullName;

/// <summary>
/// Expected hierarchy direction option values, in display order.
/// </summary>
private static readonly string[] ExpectedHierarchyDirectionOptions = ["up", "down", "both"];

/// <inheritdoc />
public void Dispose()
{
Expand Down Expand Up @@ -57,14 +62,15 @@ await File.WriteAllTextAsync(
private MainWindowShell CreateShell()
{
return new MainWindowShell(
new MainWindowShellDependencies(
new WorkspaceModel(),
new FileWatcher(TimeSpan.FromMilliseconds(1)),
new DiagnosticsAggregator(),
new ViewCatalogPresenter(),
new LayoutInvoker(),
new DiagnosticsListView(),
new SysmlSnippetGenerator(),
new RollingFileLogger(_tempLogRoot));
new RollingFileLogger(_tempLogRoot)));
}

/// <summary>
Expand Down Expand Up @@ -753,6 +759,6 @@ public void QueryDialogViewModel_QueryTypes_HasExpectedElevenEntries()
public void QueryDialogViewModel_HierarchyDirectionOptions_HasExpectedThree()
{
// Assert
Assert.Equal(new[] { "up", "down", "both" }, QueryDialogViewModel.HierarchyDirectionOptions);
Assert.Equal(ExpectedHierarchyDirectionOptions, QueryDialogViewModel.HierarchyDirectionOptions);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,14 +43,15 @@ public void Dispose()
private MainWindowShell CreateShell()
{
return new MainWindowShell(
new MainWindowShellDependencies(
new WorkspaceModel(),
new FileWatcher(TimeSpan.FromMilliseconds(1)),
new DiagnosticsAggregator(),
new ViewCatalogPresenter(),
new LayoutInvoker(),
new DiagnosticsListView(),
new SysmlSnippetGenerator(),
new RollingFileLogger(_tempLogRoot));
new RollingFileLogger(_tempLogRoot)));
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,14 +90,15 @@ await File.WriteAllTextAsync(
private MainWindowShell CreateShell()
{
return new MainWindowShell(
new MainWindowShellDependencies(
new WorkspaceModel(),
new FileWatcher(TimeSpan.FromMilliseconds(1)),
new DiagnosticsAggregator(),
new ViewCatalogPresenter(),
new LayoutInvoker(),
new DiagnosticsListView(),
new SysmlSnippetGenerator(),
new RollingFileLogger(_tempLogRoot));
new RollingFileLogger(_tempLogRoot)));
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,14 +43,15 @@ public void Dispose()
private MainWindowShell CreateShell()
{
return new MainWindowShell(
new MainWindowShellDependencies(
new WorkspaceModel(),
new FileWatcher(TimeSpan.FromMilliseconds(1)),
new DiagnosticsAggregator(),
new ViewCatalogPresenter(),
new LayoutInvoker(),
new DiagnosticsListView(),
new SysmlSnippetGenerator(),
new RollingFileLogger(_tempLogRoot));
new RollingFileLogger(_tempLogRoot)));
}

private static Task WriteFileAsync(string path, string content)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,14 +52,15 @@ await File.WriteAllTextAsync(
private MainWindowShell CreateShell()
{
return new MainWindowShell(
new MainWindowShellDependencies(
new WorkspaceModel(),
new FileWatcher(TimeSpan.FromMilliseconds(1)),
new DiagnosticsAggregator(),
new ViewCatalogPresenter(),
new LayoutInvoker(),
new DiagnosticsListView(),
new SysmlSnippetGenerator(),
new RollingFileLogger(_tempLogRoot));
new RollingFileLogger(_tempLogRoot)));
}

/// <summary>
Expand Down
Loading