diff --git a/.github/actions/push-package/action.yml b/.github/actions/push-package/action.yml index 0c40b406..57e88938 100644 --- a/.github/actions/push-package/action.yml +++ b/.github/actions/push-package/action.yml @@ -9,21 +9,21 @@ inputs: default: 'false' required: false config: - description: 'The build configuration (Build or Release)' + description: 'The build configuration (Debug or Release)' default: Release required: true version: description: 'The package version' required: true ghapikey: - description: 'The Git Hub Api Key for package feed' + description: 'The GitHub API Key for package feed' required: true nugetapikey: - description: 'The Nuget Api Key for package feed' + description: 'The NuGet API Key for package feed' required: true registry: - description: 'The package registry (oneimlx or nuget)' - default: oneimlx + description: 'The package registry (GitHub or NuGet)' + default: github required: true runs: using: "composite" @@ -33,11 +33,11 @@ runs: - name: Build run: dotnet build ./src/${{ inputs.project }}/${{ inputs.project }}.csproj -c ${{ inputs.config }} --verbosity minimal shell: bash - - # Test + + # Test and collect coverage - name: Test if: ${{ inputs.skiptest == 'false' }} - run: dotnet test ./test/${{ inputs.project }}.Tests/${{ inputs.project }}.Tests.csproj -c ${{ inputs.config }} --verbosity minimal + run: dotnet test ./test/${{ inputs.project }}.Tests/${{ inputs.project }}.Tests.csproj -c ${{ inputs.config }} --verbosity minimal --collect:"XPlat Code Coverage" --results-directory ./coverage --filter "Category!=Performance" shell: bash # Pack @@ -47,22 +47,21 @@ runs: dotnet pack ./src/${{ inputs.project }}/${{ inputs.project }}.csproj -p:PackageVersion=${{ inputs.version }} --no-build -c ${{ inputs.config }} -o _packages -v minimal shell: bash - # GITHUB - - name: Github Push - if: ${{ inputs.registry == 'oneimlx' }} - run: dotnet nuget push _packages/${{ inputs.project }}.${{ inputs.version }}.nupkg --skip-duplicate --no-symbols --api-key ${{ inputs.ghapikey }} --source oneimlx + # GitHub Feed + - name: GitHub Push + if: ${{ inputs.registry == 'github' }} + run: dotnet nuget push _packages/${{ inputs.project }}.${{ inputs.version }}.nupkg --skip-duplicate --no-symbols --api-key ${{ inputs.ghapikey }} --source github shell: bash - # NUGET - 1 - # First push to github (for package cadence, Nuget requires package verification) - - name: Github Push + # NuGet Feed + # First push to GitHub (for package cadence, NuGet requires package verification) + - name: GitHub Push if: ${{ inputs.registry == 'nuget' }} - run: dotnet nuget push _packages/${{ inputs.project }}.${{ inputs.version }}.nupkg --skip-duplicate --no-symbols --api-key ${{ inputs.ghapikey }} --source oneimlx + run: dotnet nuget push _packages/${{ inputs.project }}.${{ inputs.version }}.nupkg --skip-duplicate --no-symbols --api-key ${{ inputs.ghapikey }} --source github shell: bash - - # NUGET - 2 - # Second push to Nuget (for package cadence, Github will be used and in production Nuget after package verification) - - name: Nuget Push + + # Second push + - name: NuGet Push if: ${{ inputs.registry == 'nuget' }} run: dotnet nuget push _packages/${{ inputs.project }}.${{ inputs.version }}.nupkg --skip-duplicate --no-symbols --api-key ${{ inputs.nugetapikey }} --source nuget.org shell: bash diff --git a/.github/workflows/build-test-cross.yml b/.github/workflows/build-test-cross.yml index 888114d3..28713e88 100644 --- a/.github/workflows/build-test-cross.yml +++ b/.github/workflows/build-test-cross.yml @@ -29,7 +29,7 @@ jobs: # Checkout - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 # Read env - name: Read Env @@ -38,7 +38,7 @@ jobs: # Setup .Net with global.json - name: Setup .NET - uses: actions/setup-dotnet@v4 + uses: actions/setup-dotnet@v5 # Build - name: Restore & Build @@ -66,7 +66,7 @@ jobs: # Checkout - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v6 # Read env - name: Read Env @@ -75,17 +75,17 @@ jobs: # Setup .Net with global.json - name: Setup .NET - uses: actions/setup-dotnet@v3 - + uses: actions/setup-dotnet@v5 + # Build - name: Restore & Build run: dotnet build "OneImlx.Terminal.Solution.slnx" - + # Test - name: Test run: dotnet test "OneImlx.Terminal.Solution.slnx" --no-build --verbosity normal - - windows: + + windows: runs-on: windows-latest timeout-minutes: 15 @@ -103,7 +103,7 @@ jobs: # Checkout - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v6 # Read env - name: Read Env @@ -112,7 +112,7 @@ jobs: # Setup .Net with global.json - name: Setup .NET - uses: actions/setup-dotnet@v3 + uses: actions/setup-dotnet@v5 # Build - name: Restore & Build diff --git a/.github/workflows/build-test-push.yml b/.github/workflows/build-test-push.yml index 90331b30..f3715315 100644 --- a/.github/workflows/build-test-push.yml +++ b/.github/workflows/build-test-push.yml @@ -1,13 +1,14 @@ -# Copyright (c) 2026 Perpetual Intelligence L.L.C. All Rights Reserved. +# Copyright (c) Perpetual Intelligence L.L.C. All Rights Reserved. # # For license, terms, and data policies, go to: # https://terms.perpetualintelligence.com name: build-test-push -concurrency: github-release +# 1 release at a time +concurrency: ${{ github.event.repository.name }} -# Manual trigger for deployment +# CICD. The rc and release are a manual trigger on: push: branches: [ main ] @@ -29,9 +30,9 @@ on: description: 'The package registry' required: true options: - - oneimlx + - github - nuget - default: oneimlx + default: github jobs: setup: @@ -44,7 +45,7 @@ jobs: PI_GITHUB_USERNAME_ENV: ${{ secrets.PI_GITHUB_USERNAME }} PI_GITHUB_PAT_ENV: ${{ secrets.PI_GITHUB_PAT }} PI_NUGET_PAT_ENV: ${{ secrets.PI_NUGET_API_KEY }} - PI_PUBLISH_REGISTRY: oneimlx + PI_PUBLISH_REGISTRY: github PI_CI_REFERENCE: package PI_TERMINAL_TEST_ONLINE_LIC: ${{ secrets.PI_TERMINAL_TEST_ONLINE_LIC }} PI_TERMINAL_TEST_OFFLINE_LIC: ${{ secrets.PI_TERMINAL_TEST_OFFLINE_LIC }} @@ -52,9 +53,9 @@ jobs: steps: - # Checktout + # Checkout - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 # Print directory - name: Print @@ -120,7 +121,12 @@ jobs: # Setup .Net with global.json - name: Setup .NET - uses: actions/setup-dotnet@v4 + uses: actions/setup-dotnet@v5 + + # Setup ReportGenerator + - name: Setup ReportGenerator + run: dotnet tool install --global dotnet-reportgenerator-globaltool + shell: bash # Build Terminal.DependentAssembly.csproj so it can be loaded during unit tests. - name: Build Dependency Project @@ -128,6 +134,7 @@ jobs: # Publish OneImlx.Terminal.Shared - name: OneImlx.Terminal.Shared + id: oneimlx-terminal-shared uses: ./.github/actions/push-package with: project: OneImlx.Terminal.Shared @@ -139,6 +146,7 @@ jobs: # Publish OneImlx.Terminal - name: OneImlx.Terminal + id: oneimlx-terminal uses: ./.github/actions/push-package with: project: OneImlx.Terminal @@ -150,6 +158,7 @@ jobs: # Publish OneImlx.Terminal.Server - name: OneImlx.Terminal.Server + id: oneimlx-terminal-server uses: ./.github/actions/push-package with: project: OneImlx.Terminal.Server @@ -161,6 +170,7 @@ jobs: # Publish OneImlx.Terminal.Server.Grpc - name: OneImlx.Terminal.Server.Grpc + id: oneimlx-terminal-server-grpc uses: ./.github/actions/push-package with: project: OneImlx.Terminal.Server.Grpc @@ -172,6 +182,7 @@ jobs: # Publish OneImlx.Terminal.Server.Pulsar - name: OneImlx.Terminal.Server.Pulsar + id: oneimlx-terminal-server-pulsar uses: ./.github/actions/push-package with: project: OneImlx.Terminal.Server.Pulsar @@ -183,6 +194,7 @@ jobs: # Publish OneImlx.Terminal.Client - name: OneImlx.Terminal.Client + id: oneimlx-terminal-client uses: ./.github/actions/push-package with: project: OneImlx.Terminal.Client @@ -194,6 +206,7 @@ jobs: # Publish OneImlx.Terminal.Client.Grpc - name: OneImlx.Terminal.Client.Grpc + id: oneimlx-terminal-client-grpc uses: ./.github/actions/push-package with: project: OneImlx.Terminal.Client.Grpc @@ -203,8 +216,9 @@ jobs: nugetapikey: ${{ env.PI_NUGET_PAT_ENV }} registry: ${{ env.PI_PUBLISH_REGISTRY }} - # Publish OneImlx.Terminal.Server.Pulsar + # Publish OneImlx.Terminal.Client.Pulsar - name: OneImlx.Terminal.Client.Pulsar + id: oneimlx-terminal-client-pulsar uses: ./.github/actions/push-package with: project: OneImlx.Terminal.Client.Pulsar @@ -216,6 +230,7 @@ jobs: # Publish OneImlx.Terminal.Authentication - name: OneImlx.Terminal.Authentication + id: oneimlx-terminal-authentication uses: ./.github/actions/push-package with: project: OneImlx.Terminal.Authentication @@ -224,3 +239,42 @@ jobs: ghapikey: ${{ env.PI_GITHUB_PAT_ENV }} nugetapikey: ${{ env.PI_NUGET_PAT_ENV }} registry: ${{ env.PI_PUBLISH_REGISTRY }} + + # Aggregate coverage report across all packages deposited into ./coverage + - name: Coverage Report + run: reportgenerator -reports:./coverage/**/coverage.cobertura.xml -targetdir:./coverage/report -reporttypes:JsonSummary + shell: bash + + # Extract aggregate line and branch coverage + - name: Extract Coverage + id: coverage + run: | + echo "line=$(jq '.summary.linecoverage' ./coverage/report/Summary.json | xargs printf '%.0f')" >> $GITHUB_OUTPUT + echo "branch=$(jq '.summary.branchcoverage' ./coverage/report/Summary.json | xargs printf '%.0f')" >> $GITHUB_OUTPUT + shell: bash + + # Update line coverage badge in Gist + - name: Line Coverage Badge + uses: Schneegans/dynamic-badges-action@v1.8.0 + with: + auth: ${{ secrets.PI_GITHUB_GIST_PAT }} + gistID: ${{ secrets.PI_GITHUB_GIST_ID }} + filename: coverage_${{ github.event.repository.name }}_line.json + label: coverage-line + message: ${{ steps.coverage.outputs.line }}% + valColorRange: ${{ steps.coverage.outputs.line }} + maxColorRange: 100 + minColorRange: 0 + + # Update branch coverage badge in Gist + - name: Branch Coverage Badge + uses: Schneegans/dynamic-badges-action@v1.8.0 + with: + auth: ${{ secrets.PI_GITHUB_GIST_PAT }} + gistID: ${{ secrets.PI_GITHUB_GIST_ID }} + filename: coverage_${{ github.event.repository.name }}_branch.json + label: coverage-branch + message: ${{ steps.coverage.outputs.branch }}% + valColorRange: ${{ steps.coverage.outputs.branch }} + maxColorRange: 100 + minColorRange: 0 diff --git a/.github/workflows/delete-packages.yml b/.github/workflows/delete-packages.yml index 9dbfe079..22321273 100644 --- a/.github/workflows/delete-packages.yml +++ b/.github/workflows/delete-packages.yml @@ -32,7 +32,15 @@ on: description: 'The package name to delete' required: true options: + - OneImlx.Terminal.Shared - OneImlx.Terminal + - OneImlx.Terminal.Server + - OneImlx.Terminal.Server.Grpc + - OneImlx.Terminal.Server.Pulsar + - OneImlx.Terminal.Client + - OneImlx.Terminal.Client.Grpc + - OneImlx.Terminal.Client.Pulsar + - OneImlx.Terminal.Authentication - All default: 'All' @@ -66,15 +74,15 @@ jobs: # Delete specific - name: Delete ${{ env.PI_DELETE_PACKAGES }} if: ${{ env.PI_DELETE_PACKAGES != 'All' }} - uses: actions/delete-package-versions@v2 + uses: actions/delete-package-versions@v5 with: package-name: ${{ env.PI_DELETE_PACKAGES }} min-versions-to-keep: ${{ env.PI_KEEP_MINIMUM_VERSIONS }} - + # Delete OneImlx.Terminal.Shared - name: Delete OneImlx.Terminal.Shared if: ${{ env.PI_DELETE_PACKAGES == 'All' }} - uses: actions/delete-package-versions@v2 + uses: actions/delete-package-versions@v5 with: package-name: OneImlx.Terminal.Shared min-versions-to-keep: ${{ env.PI_KEEP_MINIMUM_VERSIONS }} @@ -82,7 +90,63 @@ jobs: # Delete OneImlx.Terminal - name: Delete OneImlx.Terminal if: ${{ env.PI_DELETE_PACKAGES == 'All' }} - uses: actions/delete-package-versions@v2 + uses: actions/delete-package-versions@v5 with: package-name: OneImlx.Terminal min-versions-to-keep: ${{ env.PI_KEEP_MINIMUM_VERSIONS }} + + # Delete OneImlx.Terminal.Server + - name: Delete OneImlx.Terminal.Server + if: ${{ env.PI_DELETE_PACKAGES == 'All' }} + uses: actions/delete-package-versions@v5 + with: + package-name: OneImlx.Terminal.Server + min-versions-to-keep: ${{ env.PI_KEEP_MINIMUM_VERSIONS }} + + # Delete OneImlx.Terminal.Server.Grpc + - name: Delete OneImlx.Terminal.Server.Grpc + if: ${{ env.PI_DELETE_PACKAGES == 'All' }} + uses: actions/delete-package-versions@v5 + with: + package-name: OneImlx.Terminal.Server.Grpc + min-versions-to-keep: ${{ env.PI_KEEP_MINIMUM_VERSIONS }} + + # Delete OneImlx.Terminal.Server.Pulsar + - name: Delete OneImlx.Terminal.Server.Pulsar + if: ${{ env.PI_DELETE_PACKAGES == 'All' }} + uses: actions/delete-package-versions@v5 + with: + package-name: OneImlx.Terminal.Server.Pulsar + min-versions-to-keep: ${{ env.PI_KEEP_MINIMUM_VERSIONS }} + + # Delete OneImlx.Terminal.Client + - name: Delete OneImlx.Terminal.Client + if: ${{ env.PI_DELETE_PACKAGES == 'All' }} + uses: actions/delete-package-versions@v5 + with: + package-name: OneImlx.Terminal.Client + min-versions-to-keep: ${{ env.PI_KEEP_MINIMUM_VERSIONS }} + + # Delete OneImlx.Terminal.Client.Grpc + - name: Delete OneImlx.Terminal.Client.Grpc + if: ${{ env.PI_DELETE_PACKAGES == 'All' }} + uses: actions/delete-package-versions@v5 + with: + package-name: OneImlx.Terminal.Client.Grpc + min-versions-to-keep: ${{ env.PI_KEEP_MINIMUM_VERSIONS }} + + # Delete OneImlx.Terminal.Client.Pulsar + - name: Delete OneImlx.Terminal.Client.Pulsar + if: ${{ env.PI_DELETE_PACKAGES == 'All' }} + uses: actions/delete-package-versions@v5 + with: + package-name: OneImlx.Terminal.Client.Pulsar + min-versions-to-keep: ${{ env.PI_KEEP_MINIMUM_VERSIONS }} + + # Delete OneImlx.Terminal.Authentication + - name: Delete OneImlx.Terminal.Authentication + if: ${{ env.PI_DELETE_PACKAGES == 'All' }} + uses: actions/delete-package-versions@v5 + with: + package-name: OneImlx.Terminal.Authentication + min-versions-to-keep: ${{ env.PI_KEEP_MINIMUM_VERSIONS }} diff --git a/README.md b/README.md index 8ab68678..64a0bf0a 100644 --- a/README.md +++ b/README.md @@ -2,25 +2,35 @@ ***Open. Flexible. Secured. Cross-Platform.*** -> **The most flexible cross-platform framework for building modern and secured terminal apps.** +> **The most flexible cross-platform framework for building modern, secured, and production-ready terminal and CLI applications on .NET.** -[![build-test-cross](https://github.com/perpetualintelligence/terminal/actions/workflows/build-test-cross.yml/badge.svg)](https://github.com/perpetualintelligence/terminal/actions/workflows/build-test-cross.yml) -[![build-test-push](https://github.com/perpetualintelligence/terminal/actions/workflows/build-test-push.yml/badge.svg)](https://github.com/perpetualintelligence/terminal/actions/workflows/build-test-push.yml) +[![build-test-cross](https://github.com/perpetualintelligence/oneterminal/actions/workflows/build-test-cross.yml/badge.svg)](https://github.com/perpetualintelligence/oneterminal/actions/workflows/build-test-cross.yml) +[![build-test-push](https://github.com/perpetualintelligence/oneterminal/actions/workflows/build-test-push.yml/badge.svg)](https://github.com/perpetualintelligence/oneterminal/actions/workflows/build-test-push.yml) -![coverage-line: 66%](https://img.shields.io/badge/coverage--line-66%25-yellow) -![coverage-branch: 60%](https://img.shields.io/badge/coverage--branch-60%25-orange) +![coverage-line](https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/perpetualintelligencegit/141903832ee52f1e9e7913300a92d507/raw/coverage_oneterminal_line.json) +![coverage-branch](https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/perpetualintelligencegit/141903832ee52f1e9e7913300a92d507/raw/coverage_oneterminal_branch.json) -`OneImlx.Terminal` is the cross-platform adaptable framework designed for crafting modern CLI and terminal applications. We cut through the terminal infrastructure complexity, letting you concentrate on building enterprise grade terminal applications with Unicode support, and extensive command hierarchies. +`OneImlx.Terminal` is the cross-platform, production-grade framework for building modern CLI and terminal applications on .NET. It eliminates terminal infrastructure complexity so you can focus on delivering rich, enterprise-grade command experiences — with full Unicode support, deep command hierarchies, real-time streaming, and secure distributed architectures. -Quickly dive in with our [test apps](https://github.com/perpetualintelligence/terminal/tree/main/apps). +Quickly dive in with our [sample apps](https://github.com/perpetualintelligence/terminal/tree/main/apps). ## Packages -![NuGet](https://img.shields.io/nuget/vpre/OneImlx.Terminal?label=OneImlx.Terminal) -![NuGet](https://img.shields.io/nuget/vpre/OneImlx.Terminal.Authentication?label=OneImlx.Terminal.Authentication) -![NuGet](https://img.shields.io/nuget/vpre/OneImlx.Terminal.Server?label=OneImlx.Terminal.Server) -![NuGet](https://img.shields.io/nuget/vpre/OneImlx.Terminal.Client?label=OneImlx.Terminal.Client) -## **Key Features**: +| Package | NuGet | +|---|---| +| `OneImlx.Terminal.Shared` | ![NuGet](https://img.shields.io/nuget/vpre/OneImlx.Terminal.Shared?label=OneImlx.Terminal.Shared) | +| `OneImlx.Terminal` | ![NuGet](https://img.shields.io/nuget/vpre/OneImlx.Terminal?label=OneImlx.Terminal) | +| `OneImlx.Terminal.Server` | ![NuGet](https://img.shields.io/nuget/vpre/OneImlx.Terminal.Server?label=OneImlx.Terminal.Server) | +| `OneImlx.Terminal.Server.Grpc` | ![NuGet](https://img.shields.io/nuget/vpre/OneImlx.Terminal.Server.Grpc?label=OneImlx.Terminal.Server.Grpc) | +| `OneImlx.Terminal.Server.Pulsar` | ![NuGet](https://img.shields.io/nuget/vpre/OneImlx.Terminal.Server.Pulsar?label=OneImlx.Terminal.Server.Pulsar) | +| `OneImlx.Terminal.Client` | ![NuGet](https://img.shields.io/nuget/vpre/OneImlx.Terminal.Client?label=OneImlx.Terminal.Client) | +| `OneImlx.Terminal.Client.Grpc` | ![NuGet](https://img.shields.io/nuget/vpre/OneImlx.Terminal.Client.Grpc?label=OneImlx.Terminal.Client.Grpc) | +| `OneImlx.Terminal.Client.Pulsar` | ![NuGet](https://img.shields.io/nuget/vpre/OneImlx.Terminal.Client.Pulsar?label=OneImlx.Terminal.Client.Pulsar) | +| `OneImlx.Terminal.Authentication` | ![NuGet](https://img.shields.io/nuget/vpre/OneImlx.Terminal.Authentication?label=OneImlx.Terminal.Authentication) | + +## Key Features + +# **Key Features**: - Utilize microservices architecture, Dependency Injection (DI), and options pattern to design and configure your terminal apps. - Access built-in features or develop your own solutions for terminal UX, input parsing, error handling, command validation, data storage, and type verification. - Opt for high-performance in-memory command storage or integrate with remote storage solutions. @@ -28,19 +38,20 @@ Quickly dive in with our [test apps](https://github.com/perpetualintelligence/te - Create deployment-agnostic CLI terminals, test locally, and deploy on-premise, cloud (public, private, or government), or in hybrid environments. - Develop enterprise-level CLI and terminal applications with secure, zero-trust architecture, complying with OAuth and OpenID Connect (OIDC) standards. - Engage with our open-source community to resolve issues and contribute to feature enhancements and documentation. -- Quickly onboard, evaluate, and test with our [demo license](https://docs.perpetualintelligence.com/articles/terminal/gs/demo.html) and ready samples on GitHub, no signup required. +- Quickly onboard, evaluate, and test with our [demo license](https://docs.perpetualintelligence.com/articles/terminal/gs/demo.html) and ready samples on GitHub, no sign-up required. -> ***Transform any .NET ecosystem feature into a modern terminal app with ease.*** +> ***Transform any .NET ecosystem feature into a powerful, production-ready terminal app.*** ## License and Pricing + Explore our licensing, redistribution, and pricing policies: - [Licensing Overview](https://docs.perpetualintelligence.com/articles/terminal/licensing/intro.html) - [Pricing](https://www.perpetualintelligence.com/products/piterminal#pricing) -> ***The full-feature `OneImlx.Terminal` framework is free for educational, and community purposes.*** - ## Documentation -For more information explore our [conceptual documentation](https://docs.perpetualintelligence.com/articles/terminal/intro.html). + +For conceptual guides, API references, and tutorials, visit our [documentation](https://docs.perpetualintelligence.com/articles/terminal/intro.html). ## Feedback -Your feedback helps shape `OneImlx.Terminal`. For suggestions, issues, or queries, please visit our [GitHub Issues](https://github.com/perpetualintelligence/oneterminal/issues). + +Your feedback shapes `OneImlx.Terminal`. For suggestions, issues, or questions, please open a [GitHub Issue](https://github.com/perpetualintelligence/terminal/issues). diff --git a/apps/auth/TestMsal/Runners/AuthUserRunner.cs b/apps/auth/TestMsal/Runners/AuthUserRunner.cs index fe9d390d..08397a73 100644 --- a/apps/auth/TestMsal/Runners/AuthUserRunner.cs +++ b/apps/auth/TestMsal/Runners/AuthUserRunner.cs @@ -51,12 +51,7 @@ public override async Task RunCommandAsync(ICommandContext await _terminalConsole.WriteLineAsync("Invoking Microsoft Graph API..."); await _terminalConsole.WriteLineColorAsync(ConsoleColor.Magenta, "The test app will not store any data."); await Task.Delay(2000); // Simulate a delay - Microsoft.Graph.Models.User? user = await graphServiceClient.Me.GetAsync(); - if (user == null) - { - throw new Exception("User not found."); - } - + Microsoft.Graph.Models.User? user = await graphServiceClient.Me.GetAsync() ?? throw new Exception("User not found."); await _terminalConsole.WriteLineAsync($"User information:"); await _terminalConsole.WriteLineAsync($"Display Name: {user.DisplayName}"); await _terminalConsole.WriteLineAsync($"User Principal Name: {user.UserPrincipalName}"); diff --git a/apps/cli/TestConsole/Checkers/Cmd3CommandChecker.cs b/apps/cli/TestConsole/Checkers/Cmd3CommandChecker.cs index 3c07b006..f4cad49c 100644 --- a/apps/cli/TestConsole/Checkers/Cmd3CommandChecker.cs +++ b/apps/cli/TestConsole/Checkers/Cmd3CommandChecker.cs @@ -1,6 +1,6 @@ -using OneImlx.Terminal.Commands; -using OneImlx.Terminal.Commands.Checkers; +using OneImlx.Terminal.Commands.Checkers; using OneImlx.Terminal.Runtime; +using OneImlx.Terminal.Shared; using System.Threading.Tasks; namespace OneImlx.Terminal.Apps.Test.Checkers diff --git a/apps/cli/TestConsole/Runners/Grp2Runner.cs b/apps/cli/TestConsole/Runners/Grp2Runner.cs index 6a85b545..496c90c5 100644 --- a/apps/cli/TestConsole/Runners/Grp2Runner.cs +++ b/apps/cli/TestConsole/Runners/Grp2Runner.cs @@ -1,6 +1,3 @@ -// Copyright © 2019-2026 Perpetual Intelligence L.L.C. All rights reserved. -// For license, terms, and data policies, go to: -// https://terms.perpetualintelligence.com/articles/intro.html using System; using System.ComponentModel.DataAnnotations; using System.Threading.Tasks; @@ -38,7 +35,7 @@ public override async Task RunCommandAsync(ICommandContext logger.LogInformation("Executing grp2 base command"); await terminalConsole.WriteLineAsync("Group 2 (CompositeGroup under grp1)"); await terminalConsole.WriteLineAsync("===================================="); - await terminalConsole.WriteLineAsync("Available subcommands:"); + await terminalConsole.WriteLineAsync("Available sub-commands:"); await terminalConsole.WriteLineAsync(" cmd4 - Command 4"); await terminalConsole.WriteLineAsync(" cmd5 - Command 5"); await terminalConsole.WriteLineAsync(" cmd6 - Command 6"); @@ -54,10 +51,10 @@ public override async Task RunCommandAsync(ICommandContext [OptionDescriptor("opt1", nameof(String), "Option 1", BehaviorFlags.None)] public async Task Cmd4Async(ICommandContext context) { - logger.LogInformation("Executing grp1 grp2 cmd4"); - string arg1 = context.GetCommand().GetRequiredArgumentValue("arg1"); - string arg2 = context.GetCommand().GetRequiredArgumentValue("arg2"); - string opt1 = context.GetCommand().GetRequiredOptionValue("opt1"); + logger.LogInformation("Cmd4 (Leaf under composite grp2)"); + context.GetCommand().TryGetArgumentValue("arg1", out string? arg1); + context.GetCommand().TryGetArgumentValue("arg2", out string? arg2); + context.GetCommand().TryGetOptionValue("opt1", out string? opt1); await terminalConsole.WriteLineAsync($"grp1 grp2 cmd4 executed: arg1={arg1}, arg2={arg2}, opt1={opt1}"); return new CommandRunnerResult(); } @@ -67,7 +64,7 @@ public async Task Cmd4Async(ICommandContext context) [ArgumentDescriptor(1, "arg1", nameof(Int32), "Integer argument", BehaviorFlags.Required)] [ArgumentValidation("arg1", typeof(RequiredAttribute))] [ArgumentValidation("arg1", typeof(RangeAttribute), 1, 100)] - [OptionDescriptor("opt1", nameof(Boolean), "Boolean option", BehaviorFlags.None)] + [OptionDescriptor("opt1", nameof(Boolean), "Boolean option", BehaviorFlags.Required)] public async Task Cmd5Async(ICommandContext context) { logger.LogInformation("Executing grp1 grp2 cmd5"); @@ -79,7 +76,7 @@ public async Task Cmd5Async(ICommandContext context) [CommandDescriptor("cmd6", "Command 6", "Command 6 in grp2.", CommandTypes.Leaf)] [CommandTags("command", "leaf")] - [ArgumentDescriptor(1, "arg1", nameof(Boolean), "Boolean argument", BehaviorFlags.None)] + [ArgumentDescriptor(1, "arg1", nameof(Boolean), "Boolean argument", BehaviorFlags.Required)] [OptionDescriptor("opt1", nameof(String), "String option", BehaviorFlags.Required)] [OptionValidation("opt1", typeof(RequiredAttribute))] public async Task Cmd6Async(ICommandContext context) diff --git a/apps/s2s/TestClient/Runners/SendTcpRunner.cs b/apps/s2s/TestClient/Runners/SendTcpRunner.cs index c738b011..a48387b4 100644 --- a/apps/s2s/TestClient/Runners/SendTcpRunner.cs +++ b/apps/s2s/TestClient/Runners/SendTcpRunner.cs @@ -87,7 +87,7 @@ private async Task ReceiveResponsesAsync(TcpClient tcpClient, int clientIndex, C if (bytesRead > 0) { - byte[][] outputs = terminalBytesParser.Split(buffer.Take(bytesRead).ToArray(), terminalOptions.Value.Router.StreamDelimiter, ignoreEmpty: true, out _); + byte[][] outputs = terminalBytesParser.Split([.. buffer.Take(bytesRead)], terminalOptions.Value.Router.StreamDelimiter, ignoreEmpty: true, out _); foreach (byte[] opt in outputs) { TerminalInputOutput? output = JsonSerializer.Deserialize(opt); diff --git a/apps/s2s/TestServer/appsettings.json b/apps/s2s/TestServer/appsettings.json index 6590bffa..acdae0d7 100644 --- a/apps/s2s/TestServer/appsettings.json +++ b/apps/s2s/TestServer/appsettings.json @@ -1,6 +1,6 @@ { "testserver": { - "mode": "tcp", // tcp, udp, http, or grpc + "mode": "http", // tcp, udp, http, or grpc "ip": "127.0.0.1", "port": 49153, "disable_response" : false diff --git a/global.json b/global.json index 6f663186..4ddb4198 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { - "version": "10.0.203", + "version": "10.0.300", "allowPrerelease": false, "rollForward": "latestFeature" } diff --git a/nuget.config b/nuget.config index c0087f8a..2651b296 100644 --- a/nuget.config +++ b/nuget.config @@ -4,12 +4,12 @@ - + - + - + diff --git a/src/OneImlx.Terminal.Server.Grpc/TerminalGrpcRouterContext.cs b/src/OneImlx.Terminal.Server.Grpc/TerminalGrpcRouterContext.cs index c74f7b09..5c6af1a9 100644 --- a/src/OneImlx.Terminal.Server.Grpc/TerminalGrpcRouterContext.cs +++ b/src/OneImlx.Terminal.Server.Grpc/TerminalGrpcRouterContext.cs @@ -2,7 +2,7 @@ // For license, terms, and data policies, go to: // https://terms.perpetualintelligence.com/articles/intro.html -using OneImlx.Terminal.Runtime; +using OneImlx.Terminal.Shared; using System.Collections.Generic; namespace OneImlx.Terminal.Server.Grpc diff --git a/src/OneImlx.Terminal.Server.Pulsar/TerminalPulsarRouterContext.cs b/src/OneImlx.Terminal.Server.Pulsar/TerminalPulsarRouterContext.cs index a78d5208..aa4a85fb 100644 --- a/src/OneImlx.Terminal.Server.Pulsar/TerminalPulsarRouterContext.cs +++ b/src/OneImlx.Terminal.Server.Pulsar/TerminalPulsarRouterContext.cs @@ -2,7 +2,7 @@ // For license, terms, and data policies, go to: // https://terms.perpetualintelligence.com/articles/intro.html -using OneImlx.Terminal.Runtime; +using OneImlx.Terminal.Shared; using System.Collections.Generic; namespace OneImlx.Terminal.Server.Pulsar diff --git a/src/OneImlx.Terminal.Server/TerminalHttpRouterContext.cs b/src/OneImlx.Terminal.Server/TerminalHttpRouterContext.cs index d238044e..d2fb6e0d 100644 --- a/src/OneImlx.Terminal.Server/TerminalHttpRouterContext.cs +++ b/src/OneImlx.Terminal.Server/TerminalHttpRouterContext.cs @@ -2,7 +2,7 @@ // For license, terms, and data policies, go to: // https://terms.perpetualintelligence.com/articles/intro.html -using OneImlx.Terminal.Runtime; +using OneImlx.Terminal.Shared; using System.Collections.Generic; using System.Net; diff --git a/src/OneImlx.Terminal.Server/TerminalTcpRouterContext.cs b/src/OneImlx.Terminal.Server/TerminalTcpRouterContext.cs index deef25e2..263d0694 100644 --- a/src/OneImlx.Terminal.Server/TerminalTcpRouterContext.cs +++ b/src/OneImlx.Terminal.Server/TerminalTcpRouterContext.cs @@ -2,7 +2,7 @@ // For license, terms, and data policies, go to: // https://terms.perpetualintelligence.com/articles/intro.html -using OneImlx.Terminal.Runtime; +using OneImlx.Terminal.Shared; using System.Collections.Generic; using System.Net; using System.Net.Sockets; diff --git a/src/OneImlx.Terminal.Server/TerminalUdpRouterContext.cs b/src/OneImlx.Terminal.Server/TerminalUdpRouterContext.cs index 73eb6705..51a3a81b 100644 --- a/src/OneImlx.Terminal.Server/TerminalUdpRouterContext.cs +++ b/src/OneImlx.Terminal.Server/TerminalUdpRouterContext.cs @@ -2,7 +2,7 @@ // For license, terms, and data policies, go to: // https://terms.perpetualintelligence.com/articles/intro.html -using OneImlx.Terminal.Runtime; +using OneImlx.Terminal.Shared; using System.Collections.Generic; using System.Net; diff --git a/src/OneImlx.Terminal/Commands/ICommandContext.cs b/src/OneImlx.Terminal.Shared/ICommandContext.cs similarity index 85% rename from src/OneImlx.Terminal/Commands/ICommandContext.cs rename to src/OneImlx.Terminal.Shared/ICommandContext.cs index 8b10da2f..ab77248f 100644 --- a/src/OneImlx.Terminal/Commands/ICommandContext.cs +++ b/src/OneImlx.Terminal.Shared/ICommandContext.cs @@ -3,11 +3,8 @@ // https://terms.perpetualintelligence.com/articles/intro.html using System.Collections.Generic; -using OneImlx.Terminal.Commands.Parsers; -using OneImlx.Terminal.Runtime; -using OneImlx.Terminal.Shared; -namespace OneImlx.Terminal.Commands +namespace OneImlx.Terminal.Shared { /// /// An abstraction for the command execution context. diff --git a/src/OneImlx.Terminal/Commands/ICommandContextFactory.cs b/src/OneImlx.Terminal.Shared/ICommandContextFactory.cs similarity index 69% rename from src/OneImlx.Terminal/Commands/ICommandContextFactory.cs rename to src/OneImlx.Terminal.Shared/ICommandContextFactory.cs index dacc7d40..8cb0965f 100644 --- a/src/OneImlx.Terminal/Commands/ICommandContextFactory.cs +++ b/src/OneImlx.Terminal.Shared/ICommandContextFactory.cs @@ -2,24 +2,22 @@ // For license, terms, and data policies, go to: // https://terms.perpetualintelligence.com/articles/intro.html -using OneImlx.Terminal.Runtime; -using OneImlx.Terminal.Shared; using System.Collections.Generic; -namespace OneImlx.Terminal.Commands +namespace OneImlx.Terminal.Shared { /// - /// An abstraction for a factory. + /// An abstraction for a factory. /// public interface ICommandContextFactory { /// - /// Creates a new instance of . + /// Creates a new instance of . /// /// The request to process. /// The terminal routing context. /// The additional router properties. - /// A new instance of . + /// A new instance of . public ICommandContext Create(CommandRequest request, TerminalRouterContext context, Dictionary properties); } } \ No newline at end of file diff --git a/src/OneImlx.Terminal.Shared/IKeyAsId.cs b/src/OneImlx.Terminal.Shared/IKeyAsId.cs index e6b009e8..25629071 100644 --- a/src/OneImlx.Terminal.Shared/IKeyAsId.cs +++ b/src/OneImlx.Terminal.Shared/IKeyAsId.cs @@ -12,6 +12,6 @@ public interface IKeyAsId /// /// The identifier. /// - public string Id { get; } + public string CommandId { get; } } } \ No newline at end of file diff --git a/src/OneImlx.Terminal.Shared/KeyAsIdCollection.cs b/src/OneImlx.Terminal.Shared/KeyAsIdCollection.cs index 26bdad7e..c88b9bbb 100644 --- a/src/OneImlx.Terminal.Shared/KeyAsIdCollection.cs +++ b/src/OneImlx.Terminal.Shared/KeyAsIdCollection.cs @@ -27,7 +27,7 @@ public KeyAsIdCollection(ITerminalTextHandler textHandler) : base(textHandler.Eq /// protected override string GetKeyForItem(TItem item) { - return item.Id; + return item.CommandId; } } } \ No newline at end of file diff --git a/src/OneImlx.Terminal/Runtime/TerminalRouterContext.cs b/src/OneImlx.Terminal.Shared/TerminalRouterContext.cs similarity index 96% rename from src/OneImlx.Terminal/Runtime/TerminalRouterContext.cs rename to src/OneImlx.Terminal.Shared/TerminalRouterContext.cs index 7ae3af58..d176f1d4 100644 --- a/src/OneImlx.Terminal/Runtime/TerminalRouterContext.cs +++ b/src/OneImlx.Terminal.Shared/TerminalRouterContext.cs @@ -6,10 +6,10 @@ using System.Diagnostics; using System.Threading; -namespace OneImlx.Terminal.Runtime +namespace OneImlx.Terminal.Shared { /// - /// The context. + /// An abstraction of terminal router context. /// public abstract class TerminalRouterContext { @@ -49,7 +49,7 @@ public abstract class TerminalRouterContext /// particularly useful in scenarios where a complete shutdown of the terminal's operational context is /// required, such as during application termination or critical error handling. /// - public CancellationToken TerminalCancellationToken { get; internal set; } + public CancellationToken TerminalCancellationToken { get; set; } /// /// Initializes a new instance. diff --git a/src/OneImlx.Terminal/Runtime/TerminalStartMode.cs b/src/OneImlx.Terminal.Shared/TerminalStartMode.cs similarity index 97% rename from src/OneImlx.Terminal/Runtime/TerminalStartMode.cs rename to src/OneImlx.Terminal.Shared/TerminalStartMode.cs index 30de44f4..b56c0518 100644 --- a/src/OneImlx.Terminal/Runtime/TerminalStartMode.cs +++ b/src/OneImlx.Terminal.Shared/TerminalStartMode.cs @@ -2,7 +2,7 @@ // For license, terms, and data policies, go to: // https://terms.perpetualintelligence.com/articles/intro.html -namespace OneImlx.Terminal.Runtime +namespace OneImlx.Terminal.Shared { /// /// The terminal start mode. diff --git a/src/OneImlx.Terminal.Shared/TerminalTextHandler.cs b/src/OneImlx.Terminal.Shared/TerminalTextHandler.cs index a9c91174..de2162d3 100644 --- a/src/OneImlx.Terminal.Shared/TerminalTextHandler.cs +++ b/src/OneImlx.Terminal.Shared/TerminalTextHandler.cs @@ -11,28 +11,22 @@ namespace OneImlx.Terminal.Shared /// /// The default . /// - public sealed class TerminalTextHandler : ITerminalTextHandler + /// + /// Initializes a new instance of the class with specified comparison and encoding. + /// + /// The string comparison to use. + /// The text encoding to use. + public sealed class TerminalTextHandler(StringComparison comparison, Encoding encoding) : ITerminalTextHandler { - /// - /// Initializes a new instance of the class with specified comparison and encoding. - /// - /// The string comparison to use. - /// The text encoding to use. - public TerminalTextHandler(StringComparison comparison, Encoding encoding) - { - Comparison = comparison; - Encoding = encoding; - } - /// /// The string comparison. /// - public StringComparison Comparison { get; } + public StringComparison Comparison { get; } = comparison; /// /// The text encoding. /// - public Encoding Encoding { get; } + public Encoding Encoding { get; } = encoding; /// public bool CharEquals(char? ch1, char? ch2) diff --git a/src/OneImlx.Terminal/Commands/Argument.cs b/src/OneImlx.Terminal/Commands/Argument.cs index fdf5da2a..5a0cce2a 100644 --- a/src/OneImlx.Terminal/Commands/Argument.cs +++ b/src/OneImlx.Terminal/Commands/Argument.cs @@ -1,13 +1,10 @@ -/* - Copyright © 2019-2025 Perpetual Intelligence L.L.C. All rights reserved. +// Copyright © 2019-2026 Perpetual Intelligence L.L.C. All rights reserved. +// For license, terms, and data policies, go to: +// https://terms.perpetualintelligence.com/articles/intro.html - For license, terms, and data policies, go to: - https://terms.perpetualintelligence.com/articles/intro.html -*/ - -using OneImlx.Terminal.Shared; using System; using System.Collections.Generic; +using OneImlx.Terminal.Shared; namespace OneImlx.Terminal.Commands { @@ -18,7 +15,7 @@ namespace OneImlx.Terminal.Commands /// /// /// An argument id is always unique within a command. By design implements the default - /// equality and using property. Thus, two + /// equality and using property. Thus, two /// arguments with the same id are equal irrespective of other property values. This is done to improve performance /// during lookup and avoid multiple arguments with same identifiers. /// @@ -55,7 +52,7 @@ public Argument(ArgumentDescriptor argumentDescriptor, object value) /// The argument id. /// /// The argument id is unique with in a command. - public string Id => Descriptor.Id; + public string CommandId => Descriptor.CommandId; /// /// The argument value. @@ -111,7 +108,7 @@ public override bool Equals(object? obj) public bool Equals(Argument? other) { return other != null && - Id == other.Id; + CommandId == other.CommandId; } /// @@ -120,7 +117,7 @@ public bool Equals(Argument? other) /// public override int GetHashCode() { - return Id.GetHashCode(); + return CommandId.GetHashCode(); } } -} +} \ No newline at end of file diff --git a/src/OneImlx.Terminal/Commands/ArgumentDescriptor.cs b/src/OneImlx.Terminal/Commands/ArgumentDescriptor.cs index 23d96a1d..8da4bad7 100644 --- a/src/OneImlx.Terminal/Commands/ArgumentDescriptor.cs +++ b/src/OneImlx.Terminal/Commands/ArgumentDescriptor.cs @@ -43,7 +43,7 @@ public ArgumentDescriptor(int order, string id, string dataType, string descript throw new System.ArgumentException($"'{nameof(description)}' cannot be null or empty.", nameof(description)); } - Id = id; + CommandId = id; DataType = dataType; Description = description; Flags = flags; @@ -53,7 +53,7 @@ public ArgumentDescriptor(int order, string id, string dataType, string descript /// /// The argument identifier. /// - public string Id { get; } + public string CommandId { get; } /// /// The argument data type. @@ -109,7 +109,7 @@ private void SetValidationRequired() /// public override string ToString() { - return $"{Id} | {DataType} | {Flags} | {Order}"; + return $"{CommandId} | {DataType} | {Flags} | {Order}"; } private IEnumerable>? valueCheckers; diff --git a/src/OneImlx.Terminal/Commands/Checkers/ArgumentChecker.cs b/src/OneImlx.Terminal/Commands/Checkers/ArgumentChecker.cs index 69299ca9..d382cab8 100644 --- a/src/OneImlx.Terminal/Commands/Checkers/ArgumentChecker.cs +++ b/src/OneImlx.Terminal/Commands/Checkers/ArgumentChecker.cs @@ -1,9 +1,6 @@ -/* - Copyright © 2019-2025 Perpetual Intelligence L.L.C. All rights reserved. - - For license, terms, and data policies, go to: - https://terms.perpetualintelligence.com/articles/intro.html -*/ +// Copyright © 2019-2026 Perpetual Intelligence L.L.C. All rights reserved. +// For license, terms, and data policies, go to: +// https://terms.perpetualintelligence.com/articles/intro.html using System; using System.ComponentModel.DataAnnotations; @@ -38,7 +35,7 @@ public async Task CheckArgumentAsync(Argument argument) // Check for null argument value if (argument.Value == null) { - throw new TerminalException(TerminalErrors.InvalidOption, "The argument value cannot be null. argument={0}", argument.Id); + throw new TerminalException(TerminalErrors.InvalidOption, "The argument value cannot be null. argument={0}", argument.CommandId); } // Check argument data type and value type @@ -62,7 +59,7 @@ public async Task CheckArgumentAsync(Argument argument) } catch (Exception ex) { - throw new TerminalException(TerminalErrors.InvalidOption, "The argument value is not valid. argument={0} value={1} info={2}", argument.Id, argument.Value, ex.Message); + throw new TerminalException(TerminalErrors.InvalidOption, "The argument value is not valid. argument={0} value={1} info={2}", argument.CommandId, argument.Value, ex.Message); } } } @@ -86,7 +83,7 @@ protected Task StrictTypeCheckingAsync(Argument argument, D catch { // Meaningful error instead of format exception - throw new TerminalException(TerminalErrors.InvalidArgument, "The argument value does not match the mapped type. argument={0} type={1} data_type={2} value_type={3} value={4}", argument.Id, mapperResult.MappedType, argument.DataType, argument.Value.GetType().Name, argument.Value); + throw new TerminalException(TerminalErrors.InvalidArgument, "The argument value does not match the mapped type. argument={0} type={1} data_type={2} value_type={3} value={4}", argument.CommandId, mapperResult.MappedType, argument.DataType, argument.Value.GetType().Name, argument.Value); } return Task.FromResult(new OptionCheckerResult(mapperResult.MappedType)); @@ -95,4 +92,4 @@ protected Task StrictTypeCheckingAsync(Argument argument, D private readonly IDataTypeMapper mapper; private readonly TerminalOptions options; } -} +} \ No newline at end of file diff --git a/src/OneImlx.Terminal/Commands/Checkers/CommandChecker.cs b/src/OneImlx.Terminal/Commands/Checkers/CommandChecker.cs index c6b49bbc..a7a434b0 100644 --- a/src/OneImlx.Terminal/Commands/Checkers/CommandChecker.cs +++ b/src/OneImlx.Terminal/Commands/Checkers/CommandChecker.cs @@ -61,7 +61,7 @@ private async Task CheckArgumentsAsync(ICommandContext context) // Check the arguments against the descriptor constraints foreach (var arg in argumentDescriptors) { - bool containsArg = command.TryGetArgument(arg.Id, out Argument? argument); + bool containsArg = command.TryGetArgument(arg.CommandId, out Argument? argument); var flags = arg.Flags; if (!containsArg) @@ -69,7 +69,7 @@ private async Task CheckArgumentsAsync(ICommandContext context) // Required argument is missing if (flags.HasFlag(BehaviorFlags.Required)) { - throw new TerminalException(TerminalErrors.MissingArgument, "The required argument is missing. command={0} argument={1}", command.Id, arg.Id); + throw new TerminalException(TerminalErrors.MissingArgument, "The required argument is missing. command={0} argument={1}", command.Id, arg.CommandId); } continue; // Skip checking the other conditions if argument isn't present @@ -78,13 +78,13 @@ private async Task CheckArgumentsAsync(ICommandContext context) // Check obsolete if (flags.HasFlag(BehaviorFlags.Obsolete) && !terminalOptions.Checker.AllowObsolete) { - throw new TerminalException(TerminalErrors.InvalidArgument, "The argument is obsolete. command={0} argument={1}", command.Id, arg.Id); + throw new TerminalException(TerminalErrors.InvalidArgument, "The argument is obsolete. command={0} argument={1}", command.Id, arg.CommandId); } // Check disabled if (flags.HasFlag(BehaviorFlags.Disabled)) { - throw new TerminalException(TerminalErrors.InvalidArgument, "The argument is disabled. command={0} argument={1}", command.Id, arg.Id); + throw new TerminalException(TerminalErrors.InvalidArgument, "The argument is disabled. command={0} argument={1}", command.Id, arg.CommandId); } // Check arg value diff --git a/src/OneImlx.Terminal/Commands/Checkers/DataTypeMapper.cs b/src/OneImlx.Terminal/Commands/Checkers/DataTypeMapper.cs index 16227c6e..df726674 100644 --- a/src/OneImlx.Terminal/Commands/Checkers/DataTypeMapper.cs +++ b/src/OneImlx.Terminal/Commands/Checkers/DataTypeMapper.cs @@ -1,9 +1,6 @@ -/* - Copyright © 2019-2025 Perpetual Intelligence L.L.C. All rights reserved. - - For license, terms, and data policies, go to: - https://terms.perpetualintelligence.com/articles/intro.html -*/ +// Copyright © 2019-2026 Perpetual Intelligence L.L.C. All rights reserved. +// For license, terms, and data policies, go to: +// https://terms.perpetualintelligence.com/articles/intro.html using System; using System.Threading.Tasks; @@ -34,7 +31,7 @@ public Task MapToTypeAsync(TValue value) { if (string.IsNullOrWhiteSpace(value.DataType)) { - throw new TerminalException(TerminalErrors.InvalidRequest, "The value data type cannot be null or whitespace. value={0}", value.Id); + throw new TerminalException(TerminalErrors.InvalidRequest, "The value data type cannot be null or whitespace. value={0}", value.CommandId); } switch (value.DataType) @@ -54,7 +51,7 @@ public Task MapToTypeAsync(TValue value) case nameof(DateTime): return MapperResultAsync(typeof(DateTime)); default: { - throw new TerminalException(TerminalErrors.InvalidRequest, "The value data type is not supported. value={0} data_type={1}", value.Id, value.DataType); + throw new TerminalException(TerminalErrors.InvalidRequest, "The value data type is not supported. value={0} data_type={1}", value.CommandId, value.DataType); } } } @@ -67,4 +64,4 @@ private Task MapperResultAsync(Type type) private readonly ILogger> logger; private readonly TerminalOptions options; } -} +} \ No newline at end of file diff --git a/src/OneImlx.Terminal/Commands/Checkers/ICommandChecker.cs b/src/OneImlx.Terminal/Commands/Checkers/ICommandChecker.cs index 909943b2..e05af281 100644 --- a/src/OneImlx.Terminal/Commands/Checkers/ICommandChecker.cs +++ b/src/OneImlx.Terminal/Commands/Checkers/ICommandChecker.cs @@ -2,6 +2,7 @@ // For license, terms, and data policies, go to: // https://terms.perpetualintelligence.com/articles/intro.html +using OneImlx.Terminal.Shared; using System.Threading.Tasks; namespace OneImlx.Terminal.Commands.Checkers diff --git a/src/OneImlx.Terminal/Commands/Checkers/OptionChecker.cs b/src/OneImlx.Terminal/Commands/Checkers/OptionChecker.cs index 76c810a8..eea2f3c1 100644 --- a/src/OneImlx.Terminal/Commands/Checkers/OptionChecker.cs +++ b/src/OneImlx.Terminal/Commands/Checkers/OptionChecker.cs @@ -1,9 +1,6 @@ -/* - Copyright © 2019-2025 Perpetual Intelligence L.L.C. All rights reserved. - - For license, terms, and data policies, go to: - https://terms.perpetualintelligence.com/articles/intro.html -*/ +// Copyright © 2019-2026 Perpetual Intelligence L.L.C. All rights reserved. +// For license, terms, and data policies, go to: +// https://terms.perpetualintelligence.com/articles/intro.html using System; using System.ComponentModel.DataAnnotations; @@ -36,7 +33,7 @@ public async Task CheckOptionAsync(Option option) // Check for null option value if (option.Value == null) { - throw new TerminalException(TerminalErrors.InvalidOption, "The option value cannot be null. option={0}", option.Id); + throw new TerminalException(TerminalErrors.InvalidOption, "The option value cannot be null. option={0}", option.CommandId); } // Check option data type and value type @@ -60,7 +57,7 @@ public async Task CheckOptionAsync(Option option) } catch (Exception ex) { - throw new TerminalException(TerminalErrors.InvalidOption, "The option value is not valid. option={0} value={1} info={2}", option.Id, option.Value, ex.Message); + throw new TerminalException(TerminalErrors.InvalidOption, "The option value is not valid. option={0} value={1} info={2}", option.CommandId, option.Value, ex.Message); } } } @@ -84,7 +81,7 @@ protected Task StrictTypeCheckingAsync(Option option, DataT catch { // Meaningful error instead of format exception - throw new TerminalException(TerminalErrors.InvalidOption, "The option value does not match the mapped type. option={0} type={1} data_type={2} value_type={3} value={4}", option.Id, mapperResult.MappedType, option.DataType, option.Value.GetType().Name, option.Value); + throw new TerminalException(TerminalErrors.InvalidOption, "The option value does not match the mapped type. option={0} type={1} data_type={2} value_type={3} value={4}", option.CommandId, mapperResult.MappedType, option.DataType, option.Value.GetType().Name, option.Value); } return Task.FromResult(new OptionCheckerResult(mapperResult.MappedType)); @@ -93,4 +90,4 @@ protected Task StrictTypeCheckingAsync(Option option, DataT private readonly IDataTypeMapper public TagIdCollection? TagIds { get; internal set; } + /// + /// The run method descriptor. + /// + public RunMethodDescriptor? RunMethod { get; internal set; } + /// public override string ToString() { diff --git a/src/OneImlx.Terminal/Commands/Handlers/CommandHandler.cs b/src/OneImlx.Terminal/Commands/Handlers/CommandHandler.cs index 1910c896..e24a7ebc 100644 --- a/src/OneImlx.Terminal/Commands/Handlers/CommandHandler.cs +++ b/src/OneImlx.Terminal/Commands/Handlers/CommandHandler.cs @@ -10,6 +10,7 @@ using OneImlx.Terminal.Events; using OneImlx.Terminal.Extensions; using OneImlx.Terminal.Runtime; +using OneImlx.Terminal.Shared; using System; using System.Threading.Tasks; @@ -53,7 +54,7 @@ public async Task HandleCommandAsync(ICommandContext context) { if (command.TryGetOption(terminalOptions.Help.OptionId, out Option? helpOption) || command.TryGetOption(terminalOptions.Help.OptionAlias, out helpOption)) { - logger.LogDebug("Found help option. option={0}", helpOption?.Id ?? "?"); + logger.LogDebug("Found help option. option={0}", helpOption?.CommandId ?? "?"); CommandRunnerResult runnerResult = await RunCommandInnerAsync(context, command, runHelp: true).ConfigureAwait(false); return (new CommandCheckerResult(), runnerResult); } diff --git a/src/OneImlx.Terminal/Commands/Handlers/CommandResolver.cs b/src/OneImlx.Terminal/Commands/Handlers/CommandResolver.cs index 7fe08766..e560ef3f 100644 --- a/src/OneImlx.Terminal/Commands/Handlers/CommandResolver.cs +++ b/src/OneImlx.Terminal/Commands/Handlers/CommandResolver.cs @@ -77,29 +77,6 @@ public IDelegateCommandRunner ResolveCommandRunner(CommandDescriptor commandDesc return runnerDelegate; } - /// - /// Resolves the command runner method for a given command descriptor. - /// - /// The command descriptor. - /// The resolved command runner method. - /// - public RunMethod ResolveCommandRunMethod(CommandDescriptor commandDescriptor) - { - if (commandDescriptor.Type != CommandTypes.Leaf) - { - throw new TerminalException(TerminalErrors.ServerError, "The command runner method is only supported for leaf commands. command={0} type={1}", commandDescriptor.Id, commandDescriptor.Type); - } - - var commandRunnerMethods = serviceDescriptors.GetRequiredService(); - bool found = commandRunnerMethods.TryGetValue(commandDescriptor.Id, out RunMethod? runnerMethod); - if (!found) - { - throw new TerminalException(TerminalErrors.ServerError, "The command runner method is not configured. command={0}", commandDescriptor.Id); - } - - return runnerMethod; - } - private readonly ILogger logger; private readonly IServiceProvider serviceDescriptors; } diff --git a/src/OneImlx.Terminal/Commands/Handlers/ICommandHandler.cs b/src/OneImlx.Terminal/Commands/Handlers/ICommandHandler.cs index cb7b64ac..7cd520a1 100644 --- a/src/OneImlx.Terminal/Commands/Handlers/ICommandHandler.cs +++ b/src/OneImlx.Terminal/Commands/Handlers/ICommandHandler.cs @@ -2,6 +2,7 @@ // For license, terms, and data policies, go to: // https://terms.perpetualintelligence.com/articles/intro.html +using OneImlx.Terminal.Shared; using System.Threading.Tasks; namespace OneImlx.Terminal.Commands.Handlers diff --git a/src/OneImlx.Terminal/Commands/Handlers/ICommandResolver.cs b/src/OneImlx.Terminal/Commands/Handlers/ICommandResolver.cs index 5ceec140..7fad5fc2 100644 --- a/src/OneImlx.Terminal/Commands/Handlers/ICommandResolver.cs +++ b/src/OneImlx.Terminal/Commands/Handlers/ICommandResolver.cs @@ -27,12 +27,5 @@ public interface ICommandResolver /// The descriptor of the command for which to resolve the runner. /// The resolved command runner instance. IDelegateCommandRunner ResolveCommandRunner(CommandDescriptor commandDescriptor); - - /// - /// Resolves the command run method associated with the specified command descriptor. - /// - /// The descriptor of the command for which to resolve the run method. - /// The resolved command run method instance. - RunMethod ResolveCommandRunMethod(CommandDescriptor commandDescriptor); } } \ No newline at end of file diff --git a/src/OneImlx.Terminal/Commands/ICommandRouter.cs b/src/OneImlx.Terminal/Commands/ICommandRouter.cs index b96ce42c..e46cccfe 100644 --- a/src/OneImlx.Terminal/Commands/ICommandRouter.cs +++ b/src/OneImlx.Terminal/Commands/ICommandRouter.cs @@ -2,6 +2,7 @@ // For license, terms, and data policies, go to: // https://terms.perpetualintelligence.com/articles/intro.html +using OneImlx.Terminal.Shared; using System.Threading.Tasks; namespace OneImlx.Terminal.Commands diff --git a/src/OneImlx.Terminal/Commands/ICommandValue.cs b/src/OneImlx.Terminal/Commands/ICommandValue.cs index 8da51634..e1e22f58 100644 --- a/src/OneImlx.Terminal/Commands/ICommandValue.cs +++ b/src/OneImlx.Terminal/Commands/ICommandValue.cs @@ -20,7 +20,7 @@ public interface ICommandValue /// /// The identifier. /// - string Id { get; } + string CommandId { get; } /// /// The actual value. diff --git a/src/OneImlx.Terminal/Commands/Option.cs b/src/OneImlx.Terminal/Commands/Option.cs index 40951fef..63d399a7 100644 --- a/src/OneImlx.Terminal/Commands/Option.cs +++ b/src/OneImlx.Terminal/Commands/Option.cs @@ -1,9 +1,6 @@ -/* - Copyright © 2019-2025 Perpetual Intelligence L.L.C. All rights reserved. - - For license, terms, and data policies, go to: - https://terms.perpetualintelligence.com/articles/intro.html -*/ +// Copyright © 2019-2026 Perpetual Intelligence L.L.C. All rights reserved. +// For license, terms, and data policies, go to: +// https://terms.perpetualintelligence.com/articles/intro.html using System; using System.Collections.Generic; @@ -18,7 +15,7 @@ namespace OneImlx.Terminal.Commands /// /// /// An option id is always unique within a command. By design implements the default equality - /// and using property. Thus, two options + /// and using property. Thus, two options /// with the same id are equal irrespective of other property values. This is done to improve performance during /// lookup and avoid multiple options with same identifiers. /// @@ -69,7 +66,7 @@ public Option(OptionDescriptor optionDescriptor, object value, bool byAlias = fa /// The option id. /// /// The option id is unique with in a command. - public string Id => Descriptor.Id; + public string CommandId => Descriptor.Id; /// /// The option value. @@ -130,7 +127,7 @@ public override bool Equals(object? obj) public bool Equals(Option? other) { return other != null && - Id == other.Id; + CommandId == other.CommandId; } /// @@ -139,7 +136,7 @@ public bool Equals(Option? other) /// public override int GetHashCode() { - return Id.GetHashCode(); + return CommandId.GetHashCode(); } } -} +} \ No newline at end of file diff --git a/src/OneImlx.Terminal/Commands/Parsers/CommandParser.cs b/src/OneImlx.Terminal/Commands/Parsers/CommandParser.cs index 194d5a76..140a9a2c 100644 --- a/src/OneImlx.Terminal/Commands/Parsers/CommandParser.cs +++ b/src/OneImlx.Terminal/Commands/Parsers/CommandParser.cs @@ -103,12 +103,7 @@ public async Task ParseCommandAsync(ICommandContext context) throw new TerminalException(TerminalErrors.MissingCommand, "The arguments were provided, but no command was found or specified."); } - ArgumentDescriptors? argumentDescriptors = parsedCommand.ArgumentDescriptors; - if (argumentDescriptors == null) - { - throw new TerminalException(TerminalErrors.UnsupportedArgument, "The command does not support arguments. command={0}", parsedCommand.Id); - } - + ArgumentDescriptors? argumentDescriptors = parsedCommand.ArgumentDescriptors ?? throw new TerminalException(TerminalErrors.UnsupportedArgument, "The command does not support arguments. command={0}", parsedCommand.Id); int currentArgCount = parsedArguments.Count; if (argumentDescriptors.Count <= currentArgCount) { @@ -131,11 +126,7 @@ public async Task ParseCommandAsync(ICommandContext context) return null; } - OptionDescriptors? optionDescriptors = commandDescriptor.OptionDescriptors; - if (optionDescriptors == null) - { - throw new TerminalException(TerminalErrors.UnsupportedOption, "The command does not support options. command={0}", commandDescriptor.Id); - } + OptionDescriptors? optionDescriptors = commandDescriptor.OptionDescriptors ?? throw new TerminalException(TerminalErrors.UnsupportedOption, "The command does not support options. command={0}", commandDescriptor.Id); // 1. An input can be either an option or an alias, but not both. // 2. If a segment is identified as an option, it must match the option ID. diff --git a/src/OneImlx.Terminal/Commands/Parsers/ICommandParser.cs b/src/OneImlx.Terminal/Commands/Parsers/ICommandParser.cs index a32cc4c2..545a0eb7 100644 --- a/src/OneImlx.Terminal/Commands/Parsers/ICommandParser.cs +++ b/src/OneImlx.Terminal/Commands/Parsers/ICommandParser.cs @@ -5,6 +5,7 @@ https://terms.perpetualintelligence.com/articles/intro.html */ +using OneImlx.Terminal.Shared; using System.Threading.Tasks; namespace OneImlx.Terminal.Commands.Parsers diff --git a/src/OneImlx.Terminal/Commands/RunMethod.cs b/src/OneImlx.Terminal/Commands/RunMethodDescriptor.cs similarity index 74% rename from src/OneImlx.Terminal/Commands/RunMethod.cs rename to src/OneImlx.Terminal/Commands/RunMethodDescriptor.cs index e3f4ba2d..d40de8da 100644 --- a/src/OneImlx.Terminal/Commands/RunMethod.cs +++ b/src/OneImlx.Terminal/Commands/RunMethodDescriptor.cs @@ -17,46 +17,46 @@ namespace OneImlx.Terminal.Commands /// a . /// /// - /// Each maps to one unique command as its execution logic. + /// Each maps to one unique command as its execution logic. /// - public sealed class RunMethod + public sealed class RunMethodDescriptor : IKeyAsId { /// - /// Initializes a new instance of the with specified command identifier and method name using reflection. + /// Initializes a new instance of the with specified command identifier and method name using reflection. /// /// The run method identifier. /// The run method name. - public RunMethod(string id, string methodName) + public RunMethodDescriptor(string id, string methodName) { - Id = id ?? throw new ArgumentNullException(nameof(id)); + CommandId = id ?? throw new ArgumentNullException(nameof(id)); MethodName = methodName ?? throw new ArgumentNullException(nameof(methodName)); } /// - /// Initializes a new instance of the with specified command identifier and the method info. + /// Initializes a new instance of the with specified command identifier and the method info. /// /// The run method identifier. /// The run method info. - public RunMethod(string id, MethodInfo methodInfo) + public RunMethodDescriptor(string id, MethodInfo methodInfo) { - Id = id ?? throw new ArgumentNullException(nameof(id)); + CommandId = id ?? throw new ArgumentNullException(nameof(id)); MethodInfo = methodInfo ?? throw new ArgumentNullException(nameof(methodInfo)); MethodName = methodInfo.Name; } /// - /// Delegates the command execution to the method using reflection. + /// Runs the command execution to the method using reflection. /// /// /// THIS METHOD IS PART OF INTERNAL INFRASTRUCTURE AND IS NOT INTENDED FOR DIRECT USE BY APPLICATION CODE. /// - public async Task DelegateRunAsync(CommandRunner commandRunner, ICommandContext context) where TResult : CommandRunnerResult + public async Task RunAsync(CommandRunner commandRunner, ICommandContext context) where TResult : CommandRunnerResult { // Ensure command matches the passed context ParsedCommand parsedCommand = context.GetParsedCommand(); - if (parsedCommand.Command.Id != Id) + if (parsedCommand.Command.Id != CommandId) { - throw new TerminalException(TerminalErrors.InvalidCommand, "The method's command is invalid. command={0}", Id); + throw new TerminalException(TerminalErrors.InvalidCommand, "The method's command is invalid. command={0}", CommandId); } if (MethodInfo != null) @@ -67,21 +67,21 @@ public async Task DelegateRunAsync(CommandRunner comm } else if (MethodName != null) { - MethodInfo methodInfo = commandRunner.GetType().GetMethod(MethodName, BindingFlags.Instance | BindingFlags.Public) ?? throw new TerminalException(TerminalErrors.InvalidCommand, "No public run method found on the command. command={0}, name={1}", Id, MethodName); + MethodInfo methodInfo = commandRunner.GetType().GetMethod(MethodName, BindingFlags.Instance | BindingFlags.Public) ?? throw new TerminalException(TerminalErrors.InvalidCommand, "No public run method found on the command. command={0}, name={1}", CommandId, MethodName); Task resultTask = (Task)methodInfo.Invoke(commandRunner, [context]); await resultTask.ConfigureAwait(false); return resultTask.Result; } else { - throw new TerminalException(TerminalErrors.InvalidCommand, "The method name or method info is not registered. command={0}", Id); + throw new TerminalException(TerminalErrors.InvalidCommand, "The method name or method info is not registered. command={0}", CommandId); } } /// - /// The method identifiers. + /// The command identifier. /// - public string Id { get; } + public string CommandId { get; } /// /// The method info. diff --git a/src/OneImlx.Terminal/Commands/RunMethods.cs b/src/OneImlx.Terminal/Commands/RunMethods.cs deleted file mode 100644 index 05bd4d12..00000000 --- a/src/OneImlx.Terminal/Commands/RunMethods.cs +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright © 2019-2026 Perpetual Intelligence L.L.C. All rights reserved. -// For license, terms, and data policies, go to: -// https://terms.perpetualintelligence.com/articles/intro.html - -using System.Collections.Generic; - -namespace OneImlx.Terminal.Commands -{ - /// - /// The collection. - /// - public sealed class RunMethods : Dictionary - { - } -} \ No newline at end of file diff --git a/src/OneImlx.Terminal/Commands/Runners/CommandRunner.cs b/src/OneImlx.Terminal/Commands/Runners/CommandRunner.cs index a363eed4..f5487723 100644 --- a/src/OneImlx.Terminal/Commands/Runners/CommandRunner.cs +++ b/src/OneImlx.Terminal/Commands/Runners/CommandRunner.cs @@ -26,8 +26,8 @@ public async Task DelegateHelpAsync(ICommandContext context Command command = context.GetCommand(); logger?.LogDebug("Run help. command={0}", command.Id); - await RunHelpAsync(context); - return new CommandRunnerResult(); + await RunHelpAsync(context).ConfigureAwait(false); + return (TResult)new CommandRunnerResult(); } /// @@ -36,10 +36,20 @@ public async Task DelegateRunAsync(ICommandContext context, this.logger = logger; Command command = context.GetCommand(); - logger?.LogDebug("Run command. command={0}", command.Id); + logger?.LogDebug("Run command. command={0} type={1}", command.Id, command.Descriptor.Type); - var result = await RunCommandAsync(context); - return (CommandRunnerResult)(object)result; + TResult result; + RunMethodDescriptor? runMethodDescriptor = command.Descriptor.RunMethod; + if (runMethodDescriptor != null) + { + result = await runMethodDescriptor.RunAsync(this, context).ConfigureAwait(false); + } + else + { + result = await RunCommandAsync(context).ConfigureAwait(false); + } + + return result; } /// diff --git a/src/OneImlx.Terminal/Commands/Runners/ICommandRunner.cs b/src/OneImlx.Terminal/Commands/Runners/ICommandRunner.cs index 38aa4003..fa47f645 100644 --- a/src/OneImlx.Terminal/Commands/Runners/ICommandRunner.cs +++ b/src/OneImlx.Terminal/Commands/Runners/ICommandRunner.cs @@ -2,6 +2,7 @@ // For license, terms, and data policies, go to: // https://terms.perpetualintelligence.com/articles/intro.html +using OneImlx.Terminal.Shared; using System.Threading.Tasks; namespace OneImlx.Terminal.Commands.Runners diff --git a/src/OneImlx.Terminal/Commands/Runners/IDelegateCommandRunner.cs b/src/OneImlx.Terminal/Commands/Runners/IDelegateCommandRunner.cs index e1d13fd1..084f92be 100644 --- a/src/OneImlx.Terminal/Commands/Runners/IDelegateCommandRunner.cs +++ b/src/OneImlx.Terminal/Commands/Runners/IDelegateCommandRunner.cs @@ -2,9 +2,10 @@ // For license, terms, and data policies, go to: // https://terms.perpetualintelligence.com/articles/intro.html -using System.Threading.Tasks; using Microsoft.Extensions.Logging; using OneImlx.Terminal.Runtime; +using OneImlx.Terminal.Shared; +using System.Threading.Tasks; namespace OneImlx.Terminal.Commands.Runners { @@ -18,7 +19,7 @@ namespace OneImlx.Terminal.Commands.Runners public interface IDelegateCommandRunner { /// - /// Delegates to asynchronously. + /// Delegates to asynchronously. /// /// The runner context. /// The help provider. @@ -27,7 +28,7 @@ public interface IDelegateCommandRunner Task DelegateHelpAsync(ICommandContext context, ITerminalHelpProvider helpProvider, ILogger? logger = null); /// - /// Delegates to asynchronously. + /// Delegates to asynchronously. /// /// The runner context. /// The logger. diff --git a/src/OneImlx.Terminal/Commands/Runners/LicenseInfoRunner.cs b/src/OneImlx.Terminal/Commands/Runners/LicenseInfoRunner.cs index 83d7db28..5a1e6b73 100644 --- a/src/OneImlx.Terminal/Commands/Runners/LicenseInfoRunner.cs +++ b/src/OneImlx.Terminal/Commands/Runners/LicenseInfoRunner.cs @@ -4,6 +4,7 @@ using OneImlx.Terminal.Licensing; using OneImlx.Terminal.Runtime; +using OneImlx.Terminal.Shared; using System; using System.Collections.Generic; using System.Threading.Tasks; diff --git a/src/OneImlx.Terminal/Extensions/ICommandBuilderExtensions.cs b/src/OneImlx.Terminal/Extensions/ICommandBuilderExtensions.cs index 47d5fc08..2362eb88 100644 --- a/src/OneImlx.Terminal/Extensions/ICommandBuilderExtensions.cs +++ b/src/OneImlx.Terminal/Extensions/ICommandBuilderExtensions.cs @@ -67,31 +67,29 @@ public static IOptionBuilder DefineOption(this ICommandBuilder builder, string i } /// - /// Starts a new definition. + /// Adds to the . /// /// The . /// The command identifier. /// The runner method name. - /// The configured . - public static IRunMethodBuilder DefineRunMethod(this ICommandBuilder builder, string id, string methodName) + /// The configured . + public static ICommandBuilder RunMethod(this ICommandBuilder builder, string id, string methodName) { - RunMethodBuilder runnerMethodBuilder = new(builder); - runnerMethodBuilder.Services.AddSingleton(new RunMethod(id, methodName)); - return runnerMethodBuilder; + builder.Services.AddSingleton(new RunMethodDescriptor(id, methodName)); + return builder; } /// - /// Starts a new definition. + /// Adds to the . /// /// The . /// The command identifier. /// The runner method info. - /// The configured . - public static IRunMethodBuilder DefineRunMethod(this ICommandBuilder builder, string id, MethodInfo methodInfo) + /// The configured . + public static ICommandBuilder RunMethod(this ICommandBuilder builder, string id, MethodInfo methodInfo) { - RunMethodBuilder runnerMethodBuilder = new(builder); - runnerMethodBuilder.Services.AddSingleton(new RunMethod(id, methodInfo)); - return runnerMethodBuilder; + builder.Services.AddSingleton(new RunMethodDescriptor(id, methodInfo)); + return builder; } /// diff --git a/src/OneImlx.Terminal/Extensions/IHostBuilderExtensions.cs b/src/OneImlx.Terminal/Extensions/IHostBuilderExtensions.cs index e48e8b3f..6477942a 100644 --- a/src/OneImlx.Terminal/Extensions/IHostBuilderExtensions.cs +++ b/src/OneImlx.Terminal/Extensions/IHostBuilderExtensions.cs @@ -4,6 +4,7 @@ using Microsoft.Extensions.Hosting; using OneImlx.Terminal.Runtime; +using OneImlx.Terminal.Shared; using System.Threading.Tasks; namespace OneImlx.Terminal.Extensions diff --git a/src/OneImlx.Terminal/Extensions/IServiceCollectionExtensions.cs b/src/OneImlx.Terminal/Extensions/IServiceCollectionExtensions.cs index aed9acde..904ca97c 100644 --- a/src/OneImlx.Terminal/Extensions/IServiceCollectionExtensions.cs +++ b/src/OneImlx.Terminal/Extensions/IServiceCollectionExtensions.cs @@ -213,21 +213,6 @@ private static ITerminalBuilder AddTerminalDefault( where THelp : class, ITerminalHelpProvider where TException : class, ITerminalExceptionHandler { - if (services == null) - { - throw new ArgumentNullException(nameof(services)); - } - - if (textHandler is null) - { - throw new ArgumentNullException(nameof(textHandler)); - } - - if (setupAction == null) - { - throw new ArgumentNullException(nameof(setupAction)); - } - services.Configure(setupAction); return services.CreateTerminalBuilder(textHandler) diff --git a/src/OneImlx.Terminal/Extensions/ITerminalBuilderExtensions.cs b/src/OneImlx.Terminal/Extensions/ITerminalBuilderExtensions.cs index 59432490..c0292929 100644 --- a/src/OneImlx.Terminal/Extensions/ITerminalBuilderExtensions.cs +++ b/src/OneImlx.Terminal/Extensions/ITerminalBuilderExtensions.cs @@ -384,9 +384,9 @@ public static ICommandBuilder DefineCommand(this ITerminalBuilder build return DefineCommand(builder, id, name, description, typeof(CommandChecker), typeof(TRunner), commandType); } - private static ITerminalBuilder AddDeclarativeRunnerInner(this ITerminalBuilder builder, Type declarativeRunner) + private static ITerminalBuilder AddDeclarativeRunnerInner(this ITerminalBuilder builder, Type declarativeOwner) { - object[] classAttrs = declarativeRunner.GetCustomAttributes(false); + object[] classAttrs = declarativeOwner.GetCustomAttributes(false); // Command descriptor The declarative runner is the command runner. ICommandDescriptorAttribute cmdAttr = GetDeclarativeInterface(classAttrs) ?? throw new TerminalException(TerminalErrors.InvalidDeclaration, "The declarative target does not define command descriptor."); @@ -395,40 +395,41 @@ private static ITerminalBuilder AddDeclarativeRunnerInner(this ITerminalBuilder Type checkerType = ProcessCommandChecker(classAttrs, typeof(CommandChecker)); // Establish command builder - ICommandBuilder commandBuilder = builder.DefineCommand(cmdAttr.Id, cmdAttr.Name, cmdAttr.Description, checkerType, declarativeRunner, cmdAttr.CommandType); + ICommandBuilder commandBuilder = builder.DefineCommand(cmdAttr.Id, cmdAttr.Name, cmdAttr.Description, checkerType, declarativeOwner, cmdAttr.CommandType); // Command runner methods if (cmdAttr.CommandType == CommandTypes.CompositeGroup) { - MethodInfo[] runnerMethods = declarativeRunner.GetMethods(BindingFlags.Public | BindingFlags.Instance); + // This is a leaf command inside a composite group so our runner is now a method not a class. + MethodInfo[] runnerMethods = declarativeOwner.GetMethods(BindingFlags.Public | BindingFlags.Instance); for (int i = 0; i < runnerMethods.Length; i++) { + // We are iterating over all the instance methods so we skip the once that does not have + // ICommandDescriptorAttribute attribute MethodInfo runnerMethod = runnerMethods[i]; object[] methodAttrs = runnerMethod.GetCustomAttributes(false); - - // Get command descriptor attribute from method ICommandDescriptorAttribute? methodCmdAttr = GetDeclarativeInterface(methodAttrs); if (methodCmdAttr == null) { continue; } - // Get command checker for method, defaults to CommandChecker if not defined - Type methodCheckerType = ProcessCommandChecker(methodAttrs, typeof(CommandChecker)); + // Get command checker for method, defaults to composite group checker if not defined + Type methodCheckerType = ProcessCommandChecker(methodAttrs, checkerType); + + // Create command descriptor for the method. + // NOTE: The runner is now a method of type(RunMethod) instead of a class, and the command checker is determined at method level. + ICommandBuilder methodCommandBuilder = builder.DefineCommand(methodCmdAttr.Id, methodCmdAttr.Name, methodCmdAttr.Description, methodCheckerType, declarativeOwner, methodCmdAttr.CommandType); - // Create command descriptor for the method - ICommandBuilder methodCommandBuilder = builder.DefineCommand(methodCmdAttr.Id, methodCmdAttr.Name, methodCmdAttr.Description, methodCheckerType, declarativeRunner, methodCmdAttr.CommandType); + // Add run method + methodCommandBuilder.RunMethod(methodCmdAttr.Id, runnerMethod); // Process method-level attributes, parent CompositeGroup as owner - ProcessCommandAttributes(methodAttrs, methodCommandBuilder, declarativeRunner, null, cmdAttr.Id); + ProcessCommandAttributes(methodAttrs, methodCommandBuilder, declarativeOwner, null, cmdAttr.Id); - // Add the method command descriptor + // Add the command descriptor methodCommandBuilder.Add(); - - // Also add the run method to the composite group - var runnerbuilder = commandBuilder.DefineRunMethod(cmdAttr.Id, runnerMethod); - runnerbuilder.Add(); } } @@ -451,7 +452,6 @@ private static void ProcessCommandAttributes(object[] attrs, ICommandBuilder com private static Type ProcessCommandChecker(object[] attrs, Type defaultChecker) { ICommandCheckerAttribute? cmdChecker = GetDeclarativeInterface(attrs); - return cmdChecker?.Checker ?? defaultChecker; } @@ -584,7 +584,7 @@ private static ICommandBuilder DefineCommand(this ITerminalBuilder builder, stri private static List GetDeclarativeInterfaces(object[] attrs) where TInterface : class { - List results = new(); + List results = []; for (int i = 0; i < attrs.Length; i++) { diff --git a/src/OneImlx.Terminal/Hosting/CommandBuilder.cs b/src/OneImlx.Terminal/Hosting/CommandBuilder.cs index 0a5260b1..9be81c6b 100644 --- a/src/OneImlx.Terminal/Hosting/CommandBuilder.cs +++ b/src/OneImlx.Terminal/Hosting/CommandBuilder.cs @@ -41,6 +41,20 @@ public ITerminalBuilder Add() ServiceProvider lsp = Services.BuildServiceProvider(); CommandDescriptor commandDescriptor = lsp.GetRequiredService(); + // Run Methods + IEnumerable runMethodDescriptors = lsp.GetServices(); + if (runMethodDescriptors.Any()) + { + if (runMethodDescriptors.Count() == 1) + { + commandDescriptor.RunMethod = runMethodDescriptors.First(); + } + else + { + throw new TerminalException(TerminalErrors.InvalidCommand, "The command cannot have multiple run methods. command_type={0} command={1}", commandDescriptor.Type, commandDescriptor.Id); + } + } + // Arguments IEnumerable argumentDescriptors = lsp.GetServices(); if (argumentDescriptors.Any()) @@ -87,8 +101,8 @@ public ITerminalBuilder Add() terminalBuilder.Services.AddSingleton(commandDescriptor); // Add the run methods to the terminal builder. - IEnumerable runMethods = lsp.GetServices(); - foreach (RunMethod runMethod in runMethods) + IEnumerable runMethods = lsp.GetServices(); + foreach (RunMethodDescriptor runMethod in runMethods) { terminalBuilder.Services.AddSingleton(runMethod); } diff --git a/src/OneImlx.Terminal/Hosting/IRunMethodBuilder.cs b/src/OneImlx.Terminal/Hosting/IRunMethodBuilder.cs deleted file mode 100644 index e01ccc85..00000000 --- a/src/OneImlx.Terminal/Hosting/IRunMethodBuilder.cs +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright © 2019-2026 Perpetual Intelligence L.L.C. All rights reserved. -// For license, terms, and data policies, go to: -// https://terms.perpetualintelligence.com/articles/intro.html - -using Microsoft.Extensions.DependencyInjection; - -namespace OneImlx.Terminal.Hosting -{ - /// - /// An abstraction of runner method builder. - /// - public interface IRunMethodBuilder - { - /// - /// The service collection. - /// - IServiceCollection Services { get; } - - /// - /// Builds a new and add it to the service collection. - /// - /// The configured . - ICommandBuilder Add(); - } -} \ No newline at end of file diff --git a/src/OneImlx.Terminal/Hosting/RunMethodBuilder.cs b/src/OneImlx.Terminal/Hosting/RunMethodBuilder.cs deleted file mode 100644 index a5bac79f..00000000 --- a/src/OneImlx.Terminal/Hosting/RunMethodBuilder.cs +++ /dev/null @@ -1,35 +0,0 @@ -// Copyright © 2019-2026 Perpetual Intelligence L.L.C. All rights reserved. -// For license, terms, and data policies, go to: -// https://terms.perpetualintelligence.com/articles/intro.html - -using Microsoft.Extensions.DependencyInjection; -using OneImlx.Terminal.Commands; - -namespace OneImlx.Terminal.Hosting -{ - /// - /// The default . - /// - public class RunMethodBuilder(ICommandBuilder builder) : IRunMethodBuilder - { - private ICommandBuilder builder = builder; - - /// - /// The service collection. - /// - public IServiceCollection Services { get; } = new ServiceCollection(); - - /// - /// Builds an and adds it to the service collection. - /// - /// The configured . - public ICommandBuilder Add() - { - // Get from local and add to - ServiceProvider lsp = Services.BuildServiceProvider(); - var runMethod = lsp.GetRequiredService(); - builder.Services.AddSingleton(runMethod); - return builder; - } - } -} \ No newline at end of file diff --git a/src/OneImlx.Terminal/Runtime/ITerminalRouter.cs b/src/OneImlx.Terminal/Runtime/ITerminalRouter.cs index 31efa6ab..f233c0a7 100644 --- a/src/OneImlx.Terminal/Runtime/ITerminalRouter.cs +++ b/src/OneImlx.Terminal/Runtime/ITerminalRouter.cs @@ -7,6 +7,7 @@ using System.Threading.Tasks; using OneImlx.Terminal.Configuration.Options; +using OneImlx.Terminal.Shared; namespace OneImlx.Terminal.Runtime { diff --git a/src/OneImlx.Terminal/Runtime/Terminal.cs b/src/OneImlx.Terminal/Runtime/Terminal.cs index f326d625..333095b0 100644 --- a/src/OneImlx.Terminal/Runtime/Terminal.cs +++ b/src/OneImlx.Terminal/Runtime/Terminal.cs @@ -6,7 +6,6 @@ using System.Runtime.CompilerServices; [assembly: InternalsVisibleTo("OneImlx.Terminal.Tests")] -[assembly: InternalsVisibleTo("OneImlx.Terminal.Server.Tests")] namespace OneImlx.Terminal.Runtime { @@ -15,20 +14,15 @@ namespace OneImlx.Terminal.Runtime /// accomplish and automate tasks on a computer without using a graphical user interface. If a CLI terminal supports /// user interaction, the UX is the terminal. /// - public sealed class Terminal + /// + /// Initializes a new instance. + /// + /// The terminal identifier. + public sealed class Terminal(string id) { - /// - /// Initializes a new instance. - /// - /// The terminal identifier. - public Terminal(string id) - { - Id = id; - } - /// /// The terminal identifier. /// - public string Id { get; } + public string Id { get; } = id; } } \ No newline at end of file diff --git a/src/OneImlx.Terminal/Runtime/TerminalConsoleExceptionHandler.cs b/src/OneImlx.Terminal/Runtime/TerminalConsoleExceptionHandler.cs index 10a18a29..a48ce060 100644 --- a/src/OneImlx.Terminal/Runtime/TerminalConsoleExceptionHandler.cs +++ b/src/OneImlx.Terminal/Runtime/TerminalConsoleExceptionHandler.cs @@ -1,15 +1,11 @@ -/* - Copyright © 2019-2025 Perpetual Intelligence L.L.C. All rights reserved. - - For license, terms, and data policies, go to: - https://terms.perpetualintelligence.com/articles/intro.html -*/ +// Copyright © 2019-2026 Perpetual Intelligence L.L.C. All rights reserved. +// For license, terms, and data policies, go to: +// https://terms.perpetualintelligence.com/articles/intro.html +using OneImlx.Terminal.Shared; using System; using System.Linq; using System.Threading.Tasks; -using OneImlx.Shared.Infrastructure; -using OneImlx.Terminal.Shared; namespace OneImlx.Terminal.Runtime { @@ -37,7 +33,7 @@ public Task HandleExceptionAsync(TerminalExceptionHandlerContext context) { if (context.Exception is TerminalException ee) { - object[] args = ee.Error.Args != null ? ee.Error.Args.Select(static e => e ?? "").ToArray() : []; + object[] args = ee.Error.Args != null ? [.. ee.Error.Args.Select(static e => e ?? "")] : []; terminalConsole.WriteLineColorAsync(ConsoleColor.Red, ee.Error.ErrorDescription, args); } else if (context.Exception is OperationCanceledException) @@ -68,4 +64,4 @@ public Task HandleExceptionAsync(TerminalExceptionHandlerContext context) private readonly ITerminalConsole terminalConsole; } -} +} \ No newline at end of file diff --git a/src/OneImlx.Terminal/Runtime/TerminalConsoleHelpProvider.cs b/src/OneImlx.Terminal/Runtime/TerminalConsoleHelpProvider.cs index f4da39d2..b2d41218 100644 --- a/src/OneImlx.Terminal/Runtime/TerminalConsoleHelpProvider.cs +++ b/src/OneImlx.Terminal/Runtime/TerminalConsoleHelpProvider.cs @@ -2,10 +2,10 @@ // For license, terms, and data policies, go to: // https://terms.perpetualintelligence.com/articles/intro.html -using System.Linq; -using System.Threading.Tasks; using OneImlx.Terminal.Commands; using OneImlx.Terminal.Configuration.Options; +using System.Linq; +using System.Threading.Tasks; namespace OneImlx.Terminal.Runtime { @@ -37,7 +37,7 @@ public async Task ProvideHelpAsync(TerminalHelpProviderContext context) await terminalConsole.WriteLineAsync("Arguments:"); foreach (ArgumentDescriptor argument in context.Command.Descriptor.ArgumentDescriptors) { - await terminalConsole.WriteLineAsync(string.Format("{0}{1} <{2}>", new string(' ', indent), argument.Id, argument.DataType)); + await terminalConsole.WriteLineAsync(string.Format("{0}{1} <{2}>", new string(' ', indent), argument.CommandId, argument.DataType)); await terminalConsole.WriteLineAsync(string.Format("{0}{1}", new string(' ', indent * 2), argument.Description)); } } @@ -46,15 +46,15 @@ public async Task ProvideHelpAsync(TerminalHelpProviderContext context) { indent = 2; await terminalConsole.WriteLineAsync("Options:"); - foreach (OptionDescriptor option in context.Command.Descriptor.OptionDescriptors.Values.Distinct()) + foreach (OptionDescriptor option in context.Command.Descriptor.OptionDescriptors.Values.Distinct()) { if (option.Alias != null) { - await terminalConsole.WriteLineAsync(string.Format("{0}{1}{1}{2}, {3}{4} <{5}>", new string(' ', indent), terminalOptions.Parser.OptionPrefix, option.Id, option.Alias, terminalOptions.Parser.OptionPrefix, option.DataType)); + await terminalConsole.WriteLineAsync(string.Format("{0}{1}{1}{2}, {1}{3} <{4}>", new string(' ', indent), terminalOptions.Parser.OptionPrefix, option.Id, option.Alias, option.DataType)); } else { - await terminalConsole.WriteLineAsync(string.Format("{0}{1}{2} <{3}>", new string(' ', indent), terminalOptions.Parser.OptionPrefix, option.Id, option.DataType)); + await terminalConsole.WriteLineAsync(string.Format("{0}{1}{1}{2} <{3}>", new string(' ', indent), terminalOptions.Parser.OptionPrefix, option.Id, option.DataType)); } await terminalConsole.WriteLineAsync(string.Format("{0}{1}", new string(' ', indent * 2), option.Description)); diff --git a/src/OneImlx.Terminal/Runtime/TerminalConsoleRouterContext.cs b/src/OneImlx.Terminal/Runtime/TerminalConsoleRouterContext.cs index bc686167..86091b84 100644 --- a/src/OneImlx.Terminal/Runtime/TerminalConsoleRouterContext.cs +++ b/src/OneImlx.Terminal/Runtime/TerminalConsoleRouterContext.cs @@ -2,6 +2,7 @@ // For license, terms, and data policies, go to: // https://terms.perpetualintelligence.com/articles/intro.html +using OneImlx.Terminal.Shared; using System.Collections.Generic; namespace OneImlx.Terminal.Runtime diff --git a/src/OneImlx.Terminal/Runtime/TerminalCustomRouterContext.cs b/src/OneImlx.Terminal/Runtime/TerminalCustomRouterContext.cs index 215afd08..d956534c 100644 --- a/src/OneImlx.Terminal/Runtime/TerminalCustomRouterContext.cs +++ b/src/OneImlx.Terminal/Runtime/TerminalCustomRouterContext.cs @@ -2,6 +2,7 @@ // For license, terms, and data policies, go to: // https://terms.perpetualintelligence.com/articles/intro.html +using OneImlx.Terminal.Shared; using System.Collections.Generic; namespace OneImlx.Terminal.Runtime diff --git a/src/OneImlx.Terminal/Runtime/TerminalHelpProviderContext.cs b/src/OneImlx.Terminal/Runtime/TerminalHelpProviderContext.cs index bdb44365..6603f145 100644 --- a/src/OneImlx.Terminal/Runtime/TerminalHelpProviderContext.cs +++ b/src/OneImlx.Terminal/Runtime/TerminalHelpProviderContext.cs @@ -1,9 +1,6 @@ -/* - Copyright © 2019-2025 Perpetual Intelligence L.L.C. All rights reserved. - - For license, terms, and data policies, go to: - https://terms.perpetualintelligence.com/articles/intro.html -*/ +// Copyright © 2019-2026 Perpetual Intelligence L.L.C. All rights reserved. +// For license, terms, and data policies, go to: +// https://terms.perpetualintelligence.com/articles/intro.html using OneImlx.Terminal.Commands; @@ -12,20 +9,15 @@ namespace OneImlx.Terminal.Runtime /// /// The context. /// - public sealed class TerminalHelpProviderContext + /// + /// Initialize a new instance. + /// + /// The command descriptor. + public sealed class TerminalHelpProviderContext(Command command) { - /// - /// Initialize a new instance. - /// - /// The command descriptor. - public TerminalHelpProviderContext(Command command) - { - Command = command; - } - /// /// The command descriptor. /// - public Command Command { get; set; } + public Command Command { get; set; } = command; } -} +} \ No newline at end of file diff --git a/src/OneImlx.Terminal/Runtime/TerminalLoggerExceptionHandler.cs b/src/OneImlx.Terminal/Runtime/TerminalLoggerExceptionHandler.cs index fc2b3d38..40aca3da 100644 --- a/src/OneImlx.Terminal/Runtime/TerminalLoggerExceptionHandler.cs +++ b/src/OneImlx.Terminal/Runtime/TerminalLoggerExceptionHandler.cs @@ -1,15 +1,11 @@ -/* - Copyright © 2019-2025 Perpetual Intelligence L.L.C. All rights reserved. +// Copyright © 2019-2026 Perpetual Intelligence L.L.C. All rights reserved. +// For license, terms, and data policies, go to: +// https://terms.perpetualintelligence.com/articles/intro.html - For license, terms, and data policies, go to: - https://terms.perpetualintelligence.com/articles/intro.html -*/ - -using System; -using System.Threading.Tasks; using Microsoft.Extensions.Logging; -using OneImlx.Shared.Infrastructure; using OneImlx.Terminal.Shared; +using System; +using System.Threading.Tasks; namespace OneImlx.Terminal.Runtime { @@ -69,4 +65,4 @@ public Task HandleExceptionAsync(TerminalExceptionHandlerContext context) private readonly ILogger logger; } -} +} \ No newline at end of file diff --git a/src/OneImlx.Terminal/Runtime/TerminalLoggerHelpProvider.cs b/src/OneImlx.Terminal/Runtime/TerminalLoggerHelpProvider.cs index a31e47cc..84eba944 100644 --- a/src/OneImlx.Terminal/Runtime/TerminalLoggerHelpProvider.cs +++ b/src/OneImlx.Terminal/Runtime/TerminalLoggerHelpProvider.cs @@ -1,15 +1,12 @@ -/* - Copyright © 2019-2025 Perpetual Intelligence L.L.C. All rights reserved. +// Copyright © 2019-2026 Perpetual Intelligence L.L.C. All rights reserved. +// For license, terms, and data policies, go to: +// https://terms.perpetualintelligence.com/articles/intro.html - For license, terms, and data policies, go to: - https://terms.perpetualintelligence.com/articles/intro.html -*/ - -using System.Linq; -using System.Threading.Tasks; using Microsoft.Extensions.Logging; using OneImlx.Terminal.Commands; using OneImlx.Terminal.Configuration.Options; +using System.Linq; +using System.Threading.Tasks; namespace OneImlx.Terminal.Runtime { @@ -41,7 +38,7 @@ public Task ProvideHelpAsync(TerminalHelpProviderContext context) _logger.LogInformation("Arguments:"); foreach (ArgumentDescriptor argument in context.Command.Descriptor.ArgumentDescriptors) { - _logger.LogInformation(string.Format("{0}{1} <{2}>", new string(' ', indent), argument.Id, argument.DataType)); + _logger.LogInformation(string.Format("{0}{1} <{2}>", new string(' ', indent), argument.CommandId, argument.DataType)); _logger.LogInformation(string.Format("{0}{1}", new string(' ', indent * 2), argument.Description)); } } @@ -54,11 +51,11 @@ public Task ProvideHelpAsync(TerminalHelpProviderContext context) { if (option.Alias != null) { - _logger.LogInformation(string.Format("{0}{1}{1}{2}, {3}{4} <{5}>", new string(' ', indent), terminalOptions.Parser.OptionPrefix, option.Id, terminalOptions.Parser.OptionPrefix, option.Alias, option.DataType)); + _logger.LogInformation(string.Format("{0}{1}{1}{2}, {1}{3} <{4}>", new string(' ', indent), terminalOptions.Parser.OptionPrefix, option.Id, option.Alias, option.DataType)); } else { - _logger.LogInformation(string.Format("{0}{1}{2} <{3}>", new string(' ', indent), terminalOptions.Parser.OptionPrefix, option.Id, option.DataType)); + _logger.LogInformation(string.Format("{0}{1}{1}{2} <{3}>", new string(' ', indent), terminalOptions.Parser.OptionPrefix, option.Id, option.DataType)); } _logger.LogInformation(string.Format("{0}{1}", new string(' ', indent * 2), option.Description)); @@ -71,4 +68,4 @@ public Task ProvideHelpAsync(TerminalHelpProviderContext context) private readonly ILogger _logger; private readonly TerminalOptions terminalOptions; } -} +} \ No newline at end of file diff --git a/src/OneImlx.Terminal/Runtime/TerminalNoConsole.cs b/src/OneImlx.Terminal/Runtime/TerminalNoConsole.cs index 2c9b65fd..43225cef 100644 --- a/src/OneImlx.Terminal/Runtime/TerminalNoConsole.cs +++ b/src/OneImlx.Terminal/Runtime/TerminalNoConsole.cs @@ -1,9 +1,6 @@ -/* - Copyright © 2019-2025 Perpetual Intelligence L.L.C. All rights reserved. - - For license, terms, and data policies, go to: - https://terms.perpetualintelligence.com/articles/intro.html -*/ +// Copyright © 2019-2026 Perpetual Intelligence L.L.C. All rights reserved. +// For license, terms, and data policies, go to: +// https://terms.perpetualintelligence.com/articles/intro.html using System; using System.Threading.Tasks; @@ -84,4 +81,4 @@ public Task WriteLineColorAsync(ConsoleColor foregroundColor, string value, para return Task.CompletedTask; } } -} +} \ No newline at end of file diff --git a/src/OneImlx.Terminal/Runtime/TerminalParsedRequest.cs b/src/OneImlx.Terminal/Runtime/TerminalParsedRequest.cs index 9028de0f..3a7e4c60 100644 --- a/src/OneImlx.Terminal/Runtime/TerminalParsedRequest.cs +++ b/src/OneImlx.Terminal/Runtime/TerminalParsedRequest.cs @@ -1,40 +1,31 @@ -/* - Copyright © 2019-2025 Perpetual Intelligence L.L.C. All rights reserved. +// Copyright © 2019-2026 Perpetual Intelligence L.L.C. All rights reserved. +// For license, terms, and data policies, go to: +// https://terms.perpetualintelligence.com/articles/intro.html - For license, terms, and data policies, go to: - https://terms.perpetualintelligence.com/articles/intro.html -*/ - -using OneImlx.Terminal.Shared; using System; using System.Collections.Generic; +using OneImlx.Terminal.Shared; namespace OneImlx.Terminal.Runtime { /// /// The raw representation of a parsed . /// - public sealed class TerminalParsedRequest + /// + /// Initializes a new instance of the class. + /// + /// The parsed tokens that represent a root, groups, command, and arguments. + /// The parsed options. + public sealed class TerminalParsedRequest(IEnumerable tokens, Dictionary> options) { - /// - /// Initializes a new instance of the class. - /// - /// The parsed tokens that represent a root, groups, command, and arguments. - /// The parsed options. - public TerminalParsedRequest(IEnumerable tokens, Dictionary> options) - { - Options = options; - Tokens = tokens; - } - /// /// Gets the parsed options. /// - public Dictionary> Options { get; } + public Dictionary> Options { get; } = options; /// /// Gets the parsed tokens that represent an ordered collection of root, groups, command, and arguments. /// - public IEnumerable Tokens { get; } + public IEnumerable Tokens { get; } = tokens; } -} +} \ No newline at end of file diff --git a/src/OneImlx.Terminal/Runtime/TerminalProcessor.cs b/src/OneImlx.Terminal/Runtime/TerminalProcessor.cs index bfd4c7a5..43460f28 100644 --- a/src/OneImlx.Terminal/Runtime/TerminalProcessor.cs +++ b/src/OneImlx.Terminal/Runtime/TerminalProcessor.cs @@ -190,7 +190,7 @@ public async Task StreamAsync(byte[] bytes, int bytesLength, string senderId, st } // Split the input stream into batches using the stream delimiter - byte[][] rawInputs = terminalBytesParser.Split(previousStream.ToArray(), terminalOptions.Value.Router.StreamDelimiter, ignoreEmpty: true, out bool endsWithDelimiter); + byte[][] rawInputs = terminalBytesParser.Split([.. previousStream], terminalOptions.Value.Router.StreamDelimiter, ignoreEmpty: true, out bool endsWithDelimiter); previousStream.Clear(); // Check if the last batch ends with the delimiter @@ -210,12 +210,7 @@ public async Task StreamAsync(byte[] bytes, int bytesLength, string senderId, st // We are good to route requests. for (int idx = 0; idx < lengthToProcess; ++idx) { - TerminalInputOutput? input = JsonSerializer.Deserialize(rawInputs[idx]); - if (input == null) - { - throw new TerminalException(TerminalErrors.InvalidRequest, "The input bytes cannot be deserialized to terminal input."); - } - + TerminalInputOutput? input = JsonSerializer.Deserialize(rawInputs[idx]) ?? throw new TerminalException(TerminalErrors.InvalidRequest, "The input bytes cannot be deserialized to terminal input."); if (input.SenderId != null && input.SenderId != senderId) { throw new TerminalException(TerminalErrors.InvalidRequest, "The sender id does not match the input."); diff --git a/src/OneImlx.Terminal/Runtime/TerminalSystemConsole.cs b/src/OneImlx.Terminal/Runtime/TerminalSystemConsole.cs index 00c63749..904ac76b 100644 --- a/src/OneImlx.Terminal/Runtime/TerminalSystemConsole.cs +++ b/src/OneImlx.Terminal/Runtime/TerminalSystemConsole.cs @@ -1,9 +1,6 @@ -/* - Copyright © 2019-2025 Perpetual Intelligence L.L.C. All rights reserved. - - For license, terms, and data policies, go to: - https://terms.perpetualintelligence.com/articles/intro.html -*/ +// Copyright © 2019-2026 Perpetual Intelligence L.L.C. All rights reserved. +// For license, terms, and data policies, go to: +// https://terms.perpetualintelligence.com/articles/intro.html using System; using System.Threading; @@ -12,7 +9,7 @@ namespace OneImlx.Terminal.Runtime { /// - /// The default implementation of that uses the system . + /// The default implementation of that uses the system . /// /// /// The is thread safe and allows multiple tasks to write to the console concurrently. @@ -216,4 +213,4 @@ public async Task WriteLineColorAsync(ConsoleColor foregroundColor, string value private readonly SemaphoreSlim oneThread; } -} +} \ No newline at end of file diff --git a/test/OneImlx.Terminal.Client.Tests/Extensions/HttpClientExtensionsTests.cs b/test/OneImlx.Terminal.Client.Tests/Extensions/HttpClientExtensionsTests.cs index d2360696..deed24e4 100644 --- a/test/OneImlx.Terminal.Client.Tests/Extensions/HttpClientExtensionsTests.cs +++ b/test/OneImlx.Terminal.Client.Tests/Extensions/HttpClientExtensionsTests.cs @@ -95,6 +95,13 @@ public async Task SendToTerminal_SendsRequest_Correctly() _capturedRequest.Method.Should().Be(HttpMethod.Post); } + [Fact] + public async Task Null_Input_Throws() + { + Func act = async () => await _httpClient.SendToTerminalAsync(null!, CancellationToken.None); + await act.Should().ThrowAsync().WithMessage("The input cannot be null. (Parameter 'input')"); + } + private readonly HttpClient _httpClient; private readonly Mock _httpMessageHandlerMock; private HttpRequestMessage? _capturedRequest; // Capture the request for content verification diff --git a/test/OneImlx.Terminal.Client.Tests/Extensions/TcpClientExtensionsTests.cs b/test/OneImlx.Terminal.Client.Tests/Extensions/TcpClientExtensionsTests.cs index dd4f1d41..9dbf94ef 100644 --- a/test/OneImlx.Terminal.Client.Tests/Extensions/TcpClientExtensionsTests.cs +++ b/test/OneImlx.Terminal.Client.Tests/Extensions/TcpClientExtensionsTests.cs @@ -2,15 +2,15 @@ // For license, terms, and data policies, go to: // https://terms.perpetualintelligence.com/articles/intro.html -using FluentAssertions; -using OneImlx.Terminal.Shared; -using OneImlx.Test.FluentAssertions; using System; using System.Net; using System.Net.Sockets; using System.Text; using System.Threading; using System.Threading.Tasks; +using FluentAssertions; +using OneImlx.Terminal.Shared; +using OneImlx.Test.FluentAssertions; using Xunit; namespace OneImlx.Terminal.Client.Extensions.Tests diff --git a/test/OneImlx.Terminal.Server.Tests/TerminalTcpRouterTests.cs b/test/OneImlx.Terminal.Server.Tests/TerminalTcpRouterTests.cs index 07f5d158..2cddd688 100644 --- a/test/OneImlx.Terminal.Server.Tests/TerminalTcpRouterTests.cs +++ b/test/OneImlx.Terminal.Server.Tests/TerminalTcpRouterTests.cs @@ -252,7 +252,7 @@ public async Task RunAsync_Processes_Multiple_Clients_Delimited_Messages_Concurr messages.Add($"id {localIdx}.{jdx}", $"test message {localIdx}.{jdx}"); } - TerminalInputOutput input = TerminalInputOutput.Batch($"batch{localIdx}", messages.Keys.Cast().ToArray(), messages.Values.Cast().ToArray()); + TerminalInputOutput input = TerminalInputOutput.Batch($"batch{localIdx}", [.. messages.Keys.Cast()], [.. messages.Values.Cast()]); return await SendTcpMessageAsync(input, routerIpEndpoint); }); diff --git a/test/OneImlx.Terminal.Testing/Class1.cs b/test/OneImlx.Terminal.Testing/Class1.cs deleted file mode 100644 index 33014f65..00000000 --- a/test/OneImlx.Terminal.Testing/Class1.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System; - -namespace OneImlx.Terminal.Testing -{ - public class Class1 - { - - } -} diff --git a/test/OneImlx.Terminal.Testing/Mocks/MockListLogger.cs b/test/OneImlx.Terminal.Testing/Mocks/MockListLogger.cs index 76b0a207..c0ea69ac 100644 --- a/test/OneImlx.Terminal.Testing/Mocks/MockListLogger.cs +++ b/test/OneImlx.Terminal.Testing/Mocks/MockListLogger.cs @@ -8,14 +8,9 @@ namespace OneImlx.Terminal.Testing.Mocks { - internal class MockListLogger : ILogger + internal class MockListLogger(List allLogMessages) : ILogger { - private readonly List logMessages; - - public MockListLogger(List allLogMessages) - { - this.logMessages = allLogMessages; - } + private readonly List logMessages = allLogMessages; public IDisposable? BeginScope(TState state) where TState : notnull { @@ -29,10 +24,7 @@ public bool IsEnabled(LogLevel logLevel) public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) { - if (formatter == null) - { - throw new ArgumentNullException(nameof(formatter)); - } + ArgumentNullException.ThrowIfNull(formatter); logMessages.Add(formatter(state, exception)); } diff --git a/test/OneImlx.Terminal.Tests/Commands/Checkers/CommandCheckerTests.cs b/test/OneImlx.Terminal.Tests/Commands/Checkers/CommandCheckerTests.cs index e7bbec22..3ab5918e 100644 --- a/test/OneImlx.Terminal.Tests/Commands/Checkers/CommandCheckerTests.cs +++ b/test/OneImlx.Terminal.Tests/Commands/Checkers/CommandCheckerTests.cs @@ -8,7 +8,6 @@ using OneImlx.Terminal.Configuration.Options; using OneImlx.Terminal.Extensions; using OneImlx.Terminal.Mocks; -using OneImlx.Terminal.Runtime; using OneImlx.Terminal.Shared; using OneImlx.Terminal.Stores; using OneImlx.Test.FluentAssertions; diff --git a/test/OneImlx.Terminal.Tests/Commands/CommandContextFactoryTests.cs b/test/OneImlx.Terminal.Tests/Commands/CommandContextFactoryTests.cs new file mode 100644 index 00000000..fa70f97e --- /dev/null +++ b/test/OneImlx.Terminal.Tests/Commands/CommandContextFactoryTests.cs @@ -0,0 +1,31 @@ +// Copyright © 2019-2026 Perpetual Intelligence L.L.C. All rights reserved. +// For license, terms, and data policies, go to: +// https://terms.perpetualintelligence.com/articles/intro.html + +using FluentAssertions; +using OneImlx.Terminal.Mocks; +using OneImlx.Terminal.Shared; +using System.Collections.Generic; +using System.Threading; +using Xunit; + +namespace OneImlx.Terminal.Commands +{ + public class CommandContextFactoryTests + { + [Fact] + public void Create_SetsRequestContextAndProperties() + { + var factory = new CommandContextFactory(); + var request = new CommandRequest("req-1", "run cmd"); + var routerContext = new MockRoutingContext(TerminalStartMode.Console, CancellationToken.None); + var properties = new Dictionary { ["key"] = "value" }; + + var result = factory.Create(request, routerContext, properties); + + result.Request.Should().BeSameAs(request); + result.RouterContext.Should().BeSameAs(routerContext); + result.Properties.Should().BeSameAs(properties); + } + } +} \ No newline at end of file diff --git a/test/OneImlx.Terminal.Tests/Commands/CommandResultTests.cs b/test/OneImlx.Terminal.Tests/Commands/CommandResultTests.cs new file mode 100644 index 00000000..9543acde --- /dev/null +++ b/test/OneImlx.Terminal.Tests/Commands/CommandResultTests.cs @@ -0,0 +1,45 @@ +// Copyright © 2019-2026 Perpetual Intelligence L.L.C. All rights reserved. +// For license, terms, and data policies, go to: +// https://terms.perpetualintelligence.com/articles/intro.html + +using FluentAssertions; +using OneImlx.Terminal.Commands.Checkers; +using OneImlx.Terminal.Commands.Runners; +using OneImlx.Terminal.Shared; +using OneImlx.Test.FluentAssertions; +using System; +using Xunit; + +namespace OneImlx.Terminal.Commands +{ + public class CommandResultTests + { + [Fact] + public void Constructor_SetsCheckerAndRunnerResult() + { + var checker = new CommandCheckerResult(); + var runner = new CommandRunnerResult(); + + var result = new CommandResult(checker, runner); + + result.CheckerResult.Should().BeSameAs(checker); + result.RunnerResult.Should().BeSameAs(runner); + } + + [Fact] + public void EnsureCheckerResult_WhenNull_Throws() + { + Action act = () => new CommandResult().EnsureCheckerResult(); + + act.Should().Throw().WithErrorCode(TerminalErrors.ServerError).WithErrorDescription("The command checker result is not set."); + } + + [Fact] + public void EnsureRunnerResult_WhenNull_Throws() + { + Action act = () => new CommandResult().EnsureRunnerResult(); + + act.Should().Throw().WithErrorCode(TerminalErrors.ServerError).WithErrorDescription("The command runner result is not set."); + } + } +} \ No newline at end of file diff --git a/test/OneImlx.Terminal.Tests/Commands/Declarative/DeclarativeRunMethodsTests.cs b/test/OneImlx.Terminal.Tests/Commands/Declarative/DeclarativeRunMethodsTests.cs index 3919b81b..2bf28810 100644 --- a/test/OneImlx.Terminal.Tests/Commands/Declarative/DeclarativeRunMethodsTests.cs +++ b/test/OneImlx.Terminal.Tests/Commands/Declarative/DeclarativeRunMethodsTests.cs @@ -4,14 +4,12 @@ using FluentAssertions; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; using OneImlx.Terminal.Commands.Checkers; using OneImlx.Terminal.Commands.Handlers.Mocks; using OneImlx.Terminal.Commands.Runners; using OneImlx.Terminal.Extensions; using OneImlx.Terminal.Hosting; using OneImlx.Terminal.Mocks; -using OneImlx.Terminal.Runtime; using OneImlx.Terminal.Shared; using System; using System.Collections.Generic; @@ -27,44 +25,64 @@ public class DeclarativeRunMethodsTests : IAsyncDisposable { public DeclarativeRunMethodsTests() { - var hostBuilder = Host.CreateDefaultBuilder([]).ConfigureServices(ConfigureServicesDelegate); - host = hostBuilder.Build(); + serviceCollection = new ServiceCollection(); terminalBuilder = new(serviceCollection, new TerminalTextHandler(StringComparison.OrdinalIgnoreCase, Encoding.ASCII)); terminalBuilder.AddDeclarativeRunner(); serviceProvider = terminalBuilder.Services.BuildServiceProvider(); - cmdDescs = serviceProvider.GetServices().ToList(); + cmdDescs = [.. serviceProvider.GetServices()]; } [Fact] - public void CompositeGroup_Registers_Three_RunMethods() + public void CompositeGroup_Has_No_RunMethod() { - serviceProvider.GetServices().Should().HaveCount(3); + CommandDescriptor commandDescriptor = cmdDescs.First(e => e.Id == "composite1"); + commandDescriptor.RunMethod.Should().BeNull(); } [Fact] - public void NonCompositeGroup_Has_No_RunMethods() + public void NonCompositeGroup_Has_No_RunMethod() { var tb = new TerminalBuilder(new ServiceCollection(), new TerminalTextHandler(StringComparison.OrdinalIgnoreCase, Encoding.ASCII)); tb.AddDeclarativeRunner(); var sp = tb.Services.BuildServiceProvider(); - sp.GetServices().Should().BeEmpty(); + + var cmdDesc = sp.GetServices(); + cmdDesc.Count().Should().Be(1); + cmdDesc.First().RunMethod.Should().BeNull(); + } + + [Fact] + public void RunMethods_Not_Registered() + { + var runMethods = serviceProvider.GetServices(); + runMethods.Count().Should().Be(3); } [Fact] - public void RunMethods_Store_MethodInfo() + public void RunMethod_Stores_MethodInfo() { - var runMethods = serviceProvider.GetServices(); + var method1 = cmdDescs.First(c => c.Id == "method1"); + method1.RunMethod.Should().NotBeNull(); + + var runMethods = serviceProvider.GetServices(); runMethods.Should().OnlyContain(rm => rm.MethodInfo != null && rm.MethodInfo.DeclaringType == typeof(MockDeclarativeRunMethodsRunner) && rm.MethodInfo.ReturnType == typeof(Task)); } [Fact] - public void RunMethods_Map_To_Parent_CompositeGroup_Id() + public void RunMethod_Map_To_CommandDescriptor() { - serviceProvider.GetServices().Should().OnlyContain(rm => rm.Id == "composite1"); + var leafs = cmdDescs.Where(c => c.Type == CommandTypes.Leaf); + leafs.Should().HaveCount(3); + + foreach (CommandDescriptor cmd in leafs) + { + cmd.RunMethod.Should().NotBeNull(); + cmd.RunMethod.CommandId.Should().Be(cmd.Id); + } } [Fact] - public void Creates_Descriptors_For_Group_And_Methods() + public void Creates_Descriptors_Maps_To_Correct_Type() { cmdDescs.Should().HaveCount(4); cmdDescs.Where(c => c.Type == CommandTypes.Leaf).Should().HaveCount(3); @@ -74,12 +92,23 @@ public void Creates_Descriptors_For_Group_And_Methods() [Fact] public void Method_Descriptors_Have_Correct_Metadata() { - var method1 = cmdDescs.First(c => c.Id == "method1"); + var methodDescriptors = cmdDescs.Where(c => c.Type == CommandTypes.Leaf); + methodDescriptors.Should().HaveCount(3); + + var method1 = methodDescriptors.First(c => c.Id == "method1"); method1.Name.Should().Be("method1_name"); - method1.Description.Should().Be("method1 description"); + method1.Description.Should().Be("method1_description"); method1.Type.Should().Be(CommandTypes.Leaf); - var method2 = cmdDescs.First(c => c.Id == "method2"); - var method3 = cmdDescs.First(c => c.Id == "method3"); + + var method2 = methodDescriptors.First(c => c.Id == "method2"); + method2.Name.Should().Be("method2_name"); + method2.Description.Should().Be("method2_description"); + method2.Type.Should().Be(CommandTypes.Leaf); + + var method3 = methodDescriptors.First(c => c.Id == "method3"); + method3.Name.Should().Be("method3_name"); + method3.Description.Should().Be("method3_description"); + method3.Type.Should().Be(CommandTypes.Leaf); } [Fact] @@ -94,8 +123,16 @@ public void Method1_Has_Tags_And_CustomProperties() public void Method_Checkers_Default_Or_Override() { cmdDescs.First(c => c.Id == "method1").Checker.Should().Be(); - cmdDescs.First(c => c.Id == "method2").Checker.Should().Be(); - cmdDescs.First(c => c.Id == "method3").Checker.Should().Be(typeof(CommandChecker)); + cmdDescs.First(c => c.Id == "method2").Checker.Should().Be(); + cmdDescs.First(c => c.Id == "method3").Checker.Should().Be(); + } + + [Fact] + public void Method_Runner_Is_Always_CompositeRunner() + { + cmdDescs.First(c => c.Id == "method1").Runner.Should().Be(); + cmdDescs.First(c => c.Id == "method2").Runner.Should().Be(); + cmdDescs.First(c => c.Id == "method3").Runner.Should().Be(); } [Fact] @@ -104,12 +141,12 @@ public void Method1_Has_Arguments_And_Options() var method1 = cmdDescs.First(c => c.Id == "method1"); method1.ArgumentDescriptors.Should().HaveCount(2); var argDescs = method1.ArgumentDescriptors!; - argDescs["m1_arg1"].Id.Should().Be("m1_arg1"); + argDescs["m1_arg1"].CommandId.Should().Be("m1_arg1"); argDescs["m1_arg1"].Order.Should().Be(1); argDescs["m1_arg1"].DataType.Should().Be(nameof(String)); argDescs["m1_arg1"].Description.Should().Be("method1 arg1 desc"); argDescs["m1_arg1"].Flags.Should().Be(BehaviorFlags.None); - argDescs["m1_arg2"].Id.Should().Be("m1_arg2"); + argDescs["m1_arg2"].CommandId.Should().Be("m1_arg2"); argDescs["m1_arg2"].Order.Should().Be(2); argDescs["m1_arg2"].DataType.Should().Be(nameof(Int32)); argDescs["m1_arg2"].Description.Should().Be("method1 arg2 desc"); @@ -136,9 +173,9 @@ public void Method2_Has_Arguments_And_Options() { var method2 = cmdDescs.First(c => c.Id == "method2"); method2.ArgumentDescriptors.Should().HaveCount(2); - method2.ArgumentDescriptors!["m2_arg1"].Id.Should().Be("m2_arg1"); + method2.ArgumentDescriptors!["m2_arg1"].CommandId.Should().Be("m2_arg1"); method2.ArgumentDescriptors["m2_arg1"].Flags.Should().Be(BehaviorFlags.Required | BehaviorFlags.Obsolete); - method2.ArgumentDescriptors["m2_arg2"].Id.Should().Be("m2_arg2"); + method2.ArgumentDescriptors["m2_arg2"].CommandId.Should().Be("m2_arg2"); method2.ArgumentDescriptors["m2_arg2"].Flags.Should().Be(BehaviorFlags.None); method2.OptionDescriptors.Should().HaveCount(1); method2.OptionDescriptors!["m2_opt1"].Id.Should().Be("m2_opt1"); @@ -150,9 +187,9 @@ public void Method3_Has_Arguments_And_Options() { var method3 = cmdDescs.First(c => c.Id == "method3"); method3.ArgumentDescriptors.Should().HaveCount(3); - method3.ArgumentDescriptors!["m3_arg1"].Id.Should().Be("m3_arg1"); - method3.ArgumentDescriptors["m3_arg2"].Id.Should().Be("m3_arg2"); - method3.ArgumentDescriptors["m3_arg3"].Id.Should().Be("m3_arg3"); + method3.ArgumentDescriptors!["m3_arg1"].CommandId.Should().Be("m3_arg1"); + method3.ArgumentDescriptors["m3_arg2"].CommandId.Should().Be("m3_arg2"); + method3.ArgumentDescriptors["m3_arg3"].CommandId.Should().Be("m3_arg3"); method3.ArgumentDescriptors["m3_arg3"].Flags.Should().Be(BehaviorFlags.Required | BehaviorFlags.Disabled); method3.OptionDescriptors.Should().HaveCount(3); method3.OptionDescriptors!["m3_opt1"].Id.Should().Be("m3_opt1"); @@ -183,16 +220,9 @@ public void Commands_Have_Correct_Owners() public ValueTask DisposeAsync() { - host.Dispose(); return new ValueTask(Task.CompletedTask); } - private void ConfigureServicesDelegate(IServiceCollection opt2) - { - serviceCollection = opt2; - } - - private readonly IHost host; private readonly TerminalBuilder terminalBuilder; private IServiceCollection serviceCollection = null!; private readonly ServiceProvider serviceProvider; diff --git a/test/OneImlx.Terminal.Tests/Commands/Declarative/DeclarativeRunnerTests.cs b/test/OneImlx.Terminal.Tests/Commands/Declarative/DeclarativeRunnerTests.cs index c3a7567b..19007f19 100644 --- a/test/OneImlx.Terminal.Tests/Commands/Declarative/DeclarativeRunnerTests.cs +++ b/test/OneImlx.Terminal.Tests/Commands/Declarative/DeclarativeRunnerTests.cs @@ -184,14 +184,14 @@ public void Build_ShouldRead_Arguments_And_Options_Correctly() var argDescs = cmdDescs.First().ArgumentDescriptors; argDescs.Should().NotBeNull(); - argDescs!["arg1"].Id.Should().Be("arg1"); + argDescs!["arg1"].CommandId.Should().Be("arg1"); argDescs["arg1"].Order.Should().Be(1); argDescs["arg1"].DataType.Should().Be(nameof(String)); argDescs["arg1"].Description.Should().Be("test arg desc1"); argDescs["arg1"].Flags.Should().Be(BehaviorFlags.None); argDescs["arg1"].ValueCheckers.Should().BeNull(); - argDescs["arg2"].Id.Should().Be("arg2"); + argDescs["arg2"].CommandId.Should().Be("arg2"); argDescs["arg2"].Order.Should().Be(2); argDescs["arg2"].DataType.Should().Be(nameof(String)); argDescs["arg2"].Description.Should().Be("test arg desc2"); @@ -201,7 +201,7 @@ public void Build_ShouldRead_Arguments_And_Options_Correctly() argDescs["arg2"].ValueCheckers!.Cast>().First().ValidationAttribute.Should().BeOfType(); argDescs["arg2"].ValueCheckers!.Cast>().Last().ValidationAttribute.Should().BeOfType(); - argDescs["arg3"].Id.Should().Be("arg3"); + argDescs["arg3"].CommandId.Should().Be("arg3"); argDescs["arg3"].Order.Should().Be(3); argDescs["arg3"].DataType.Should().Be(nameof(Double)); argDescs["arg3"].Description.Should().Be("test arg desc3"); diff --git a/test/OneImlx.Terminal.Tests/Commands/Declarative/MockDeclarativeRunMethodsRunner.cs b/test/OneImlx.Terminal.Tests/Commands/Declarative/MockDeclarativeRunMethodsRunner.cs index 4ec0e03a..58985151 100644 --- a/test/OneImlx.Terminal.Tests/Commands/Declarative/MockDeclarativeRunMethodsRunner.cs +++ b/test/OneImlx.Terminal.Tests/Commands/Declarative/MockDeclarativeRunMethodsRunner.cs @@ -16,8 +16,8 @@ namespace OneImlx.Terminal.Commands.Declarative { [CommandOwners("owner1", "owner2")] - [CommandDescriptor("composite1", "composite_name1", "composite description", CommandTypes.CompositeGroup)] - [CommandChecker(typeof(MockCommandChecker))] + [CommandDescriptor("composite1", "composite_name1", "composite_description", CommandTypes.CompositeGroup)] + [CommandChecker(typeof(MockCommandCheckerInner))] [CommandTags("comp_tag1", "comp_tag2")] [CommandCustomProperty("comp_key1", "comp_value1")] [CommandCustomProperty("comp_key2", "comp_value2")] @@ -31,7 +31,7 @@ internal class MockDeclarativeRunMethodsRunner : IDeclarativeRunner public string? LastMethodCalled { get; private set; } - [CommandDescriptor("method1", "method1_name", "method1 description", CommandTypes.Leaf)] + [CommandDescriptor("method1", "method1_name", "method1_description", CommandTypes.Leaf)] [CommandChecker(typeof(MockCommandChecker))] [CommandTags("m1_tag1", "m1_tag2", "m1_tag3")] [CommandCustomProperty("m1_key1", "m1_value1")] @@ -50,8 +50,8 @@ public Task TestMethod1Async(ICommandContext context) return Task.FromResult(new CommandRunnerResult()); } - [CommandDescriptor("method2", "method2_name", "method2 description", CommandTypes.Leaf)] - [CommandChecker(typeof(MockCommandCheckerInner))] + [CommandDescriptor("method2", "method2_name", "method2_description", CommandTypes.Leaf)] + [CommandChecker(typeof(MockCommandChecker))] [CommandTags("m2_tag1")] [CommandCustomProperty("m2_key1", "m2_value1")] [OptionDescriptor("m2_opt1", nameof(Double), "method2 option1 desc", BehaviorFlags.Disabled)] @@ -67,7 +67,7 @@ public Task TestMethod2Async(ICommandContext context) return Task.FromResult(new CommandRunnerResult()); } - [CommandDescriptor("method3", "method3_name", "method3 description", CommandTypes.Leaf)] + [CommandDescriptor("method3", "method3_name", "method3_description", CommandTypes.Leaf)] [CommandTags("m3_tag1", "m3_tag2")] [CommandCustomProperty("m3_key1", "m3_value1")] [CommandCustomProperty("m3_key2", "m3_value2")] diff --git a/test/OneImlx.Terminal.Tests/Commands/Handlers/CommandHandlerTests.cs b/test/OneImlx.Terminal.Tests/Commands/Handlers/CommandHandlerTests.cs index 77cc284d..d6201d86 100644 --- a/test/OneImlx.Terminal.Tests/Commands/Handlers/CommandHandlerTests.cs +++ b/test/OneImlx.Terminal.Tests/Commands/Handlers/CommandHandlerTests.cs @@ -10,7 +10,6 @@ using OneImlx.Terminal.Configuration.Options; using OneImlx.Terminal.Extensions; using OneImlx.Terminal.Mocks; -using OneImlx.Terminal.Runtime; using OneImlx.Terminal.Shared; using OneImlx.Test.FluentAssertions; using System; diff --git a/test/OneImlx.Terminal.Tests/Commands/Handlers/CommandResolverTests.cs b/test/OneImlx.Terminal.Tests/Commands/Handlers/CommandResolverTests.cs index e9a97bf5..4a7879d3 100644 --- a/test/OneImlx.Terminal.Tests/Commands/Handlers/CommandResolverTests.cs +++ b/test/OneImlx.Terminal.Tests/Commands/Handlers/CommandResolverTests.cs @@ -28,11 +28,6 @@ public CommandResolverTests() services.AddTransient(); services.AddTransient(); - RunMethods runMethods = []; - runMethods.Add("test", new RunMethod("test", "test_method1")); - runMethods.Add("test2", new RunMethod("test2", "test_method2")); - services.AddSingleton(runMethods); - _serviceProvider = services.BuildServiceProvider(); _logger = new NullLogger(); _commandRuntime = new CommandResolver(_serviceProvider, _logger); @@ -48,8 +43,8 @@ public void NullChecker_ResolveChecker_Throws() Action act = () => _commandRuntime.ResolveCommandChecker(commandDescriptor); act.Should().Throw() - .WithErrorCode("server_error") - .WithErrorDescription("The command checker is not configured. command=test"); + .WithErrorCode("server_error") + .WithErrorDescription("The command checker is not configured. command=test"); } [Fact] @@ -62,8 +57,8 @@ public void NullRunner_ResolveRunner_Throws() Action act = () => _commandRuntime.ResolveCommandRunner(commandDescriptor); act.Should().Throw() - .WithErrorCode("server_error") - .WithErrorDescription("The command runner delegate is not configured. command=test"); + .WithErrorCode("server_error") + .WithErrorDescription("The command runner delegate is not configured. command=test"); } [Fact] @@ -123,30 +118,5 @@ public void InvalidTypeAsRunner_ResolveRunner_Throws() .WithErrorCode("server_error") .WithErrorDescription("The command runner delegate is not valid. command=invalidTypeRunner runner=MockCommandCheckerInner"); } - - [Fact] - public void NonLeafCommand_ResolveCommandRunMethod_Throws() - { - CommandDescriptor commandDescriptor = new("test", "test_name", "test_desc", CommandTypes.IsolatedGroup); - Action act = () => _commandRuntime.ResolveCommandRunMethod(commandDescriptor); - act.Should().Throw().WithErrorCode("server_error").WithErrorDescription("The command runner method is only supported for leaf commands. command=test type=2"); - } - - [Fact] - public void NotFoundRunMethod_ResolveCommandRunMethod_Throws() - { - CommandDescriptor commandDescriptor = new("not_found", "test_name", "test_desc", CommandTypes.Leaf); - Action act = () => _commandRuntime.ResolveCommandRunMethod(commandDescriptor); - act.Should().Throw().WithErrorCode("server_error").WithErrorDescription("The command runner method is not configured. command=not_found"); - } - - [Fact] - public void ValidCommandId_ResolveCommandRunMethod_Finds() - { - CommandDescriptor commandDescriptor = new("test2", "test_name2", "test_desc2", CommandTypes.Leaf); - var runMethod = _commandRuntime.ResolveCommandRunMethod(commandDescriptor); - runMethod.Id.Should().Be("test2"); - runMethod.MethodName.Should().Be("test_method2"); - } } } \ No newline at end of file diff --git a/test/OneImlx.Terminal.Tests/Commands/Handlers/Mocks/MockCommandRunnerGenericsInner.cs b/test/OneImlx.Terminal.Tests/Commands/Handlers/Mocks/MockCommandRunnerGenericsInner.cs index 38c20cd5..c7b5d894 100644 --- a/test/OneImlx.Terminal.Tests/Commands/Handlers/Mocks/MockCommandRunnerGenericsInner.cs +++ b/test/OneImlx.Terminal.Tests/Commands/Handlers/Mocks/MockCommandRunnerGenericsInner.cs @@ -1,9 +1,6 @@ -/* - Copyright © 2019-2025 Perpetual Intelligence L.L.C. All rights reserved. - - For license, terms, and data policies, go to: - https://terms.perpetualintelligence.com/articles/intro.html -*/ +// Copyright © 2019-2026 Perpetual Intelligence L.L.C. All rights reserved. +// For license, terms, and data policies, go to: +// https://terms.perpetualintelligence.com/articles/intro.html using System.Threading.Tasks; using OneImlx.Terminal.Commands.Runners; @@ -29,4 +26,4 @@ public override Task RunCommandAsync(ICommandCon return Task.FromResult(new MockGenericCommandRunnerResult()); } } -} +} \ No newline at end of file diff --git a/test/OneImlx.Terminal.Tests/Commands/Handlers/Mocks/MockCommandRunnerInner.cs b/test/OneImlx.Terminal.Tests/Commands/Handlers/Mocks/MockCommandRunnerInner.cs index dc4d0f94..ca18c584 100644 --- a/test/OneImlx.Terminal.Tests/Commands/Handlers/Mocks/MockCommandRunnerInner.cs +++ b/test/OneImlx.Terminal.Tests/Commands/Handlers/Mocks/MockCommandRunnerInner.cs @@ -2,11 +2,12 @@ // For license, terms, and data policies, go to: // https://terms.perpetualintelligence.com/articles/intro.html -using System.Threading.Tasks; using Microsoft.Extensions.Logging; using OneImlx.Terminal.Commands.Runners; using OneImlx.Terminal.Extensions; using OneImlx.Terminal.Runtime; +using OneImlx.Terminal.Shared; +using System.Threading.Tasks; namespace OneImlx.Terminal.Commands.Handlers.Mocks { diff --git a/test/OneImlx.Terminal.Tests/Commands/Handlers/Mocks/MockErrorCommandRunnerInner.cs b/test/OneImlx.Terminal.Tests/Commands/Handlers/Mocks/MockErrorCommandRunnerInner.cs index d95d8469..d8b59f1e 100644 --- a/test/OneImlx.Terminal.Tests/Commands/Handlers/Mocks/MockErrorCommandRunnerInner.cs +++ b/test/OneImlx.Terminal.Tests/Commands/Handlers/Mocks/MockErrorCommandRunnerInner.cs @@ -1,15 +1,12 @@ -/* - Copyright © 2019-2025 Perpetual Intelligence L.L.C. All rights reserved. +// Copyright © 2019-2026 Perpetual Intelligence L.L.C. All rights reserved. +// For license, terms, and data policies, go to: +// https://terms.perpetualintelligence.com/articles/intro.html - For license, terms, and data policies, go to: - https://terms.perpetualintelligence.com/articles/intro.html -*/ - -using System.Threading.Tasks; using Microsoft.Extensions.Logging; using OneImlx.Terminal.Commands.Runners; using OneImlx.Terminal.Runtime; using OneImlx.Terminal.Shared; +using System.Threading.Tasks; namespace OneImlx.Terminal.Commands.Handlers.Mocks { @@ -36,4 +33,4 @@ public Task RunHelpAsync(ICommandContext context) throw new TerminalException("test_runner_help_error", "test_runner_help_error_desc"); } } -} +} \ No newline at end of file diff --git a/test/OneImlx.Terminal.Tests/Commands/MockRunnerWithBaseResult.cs b/test/OneImlx.Terminal.Tests/Commands/MockRunnerWithBaseResult.cs index 33927562..d2caf43d 100644 --- a/test/OneImlx.Terminal.Tests/Commands/MockRunnerWithBaseResult.cs +++ b/test/OneImlx.Terminal.Tests/Commands/MockRunnerWithBaseResult.cs @@ -3,6 +3,7 @@ // https://terms.perpetualintelligence.com/articles/intro.html using OneImlx.Terminal.Commands.Runners; +using OneImlx.Terminal.Shared; using System.Threading.Tasks; namespace OneImlx.Terminal.Commands diff --git a/test/OneImlx.Terminal.Tests/Commands/MockRunnerWithDerivedResult.cs b/test/OneImlx.Terminal.Tests/Commands/MockRunnerWithDerivedResult.cs index cd064e44..ca243b6b 100644 --- a/test/OneImlx.Terminal.Tests/Commands/MockRunnerWithDerivedResult.cs +++ b/test/OneImlx.Terminal.Tests/Commands/MockRunnerWithDerivedResult.cs @@ -3,6 +3,7 @@ // https://terms.perpetualintelligence.com/articles/intro.html using OneImlx.Terminal.Commands.Runners; +using OneImlx.Terminal.Shared; using System.Threading.Tasks; namespace OneImlx.Terminal.Commands diff --git a/test/OneImlx.Terminal.Tests/Commands/Parsers/CommandParserTests.cs b/test/OneImlx.Terminal.Tests/Commands/Parsers/CommandParserTests.cs index baebd1ae..8a7c4ad2 100644 --- a/test/OneImlx.Terminal.Tests/Commands/Parsers/CommandParserTests.cs +++ b/test/OneImlx.Terminal.Tests/Commands/Parsers/CommandParserTests.cs @@ -87,8 +87,8 @@ public async Task Arguments_And_Options_Processed_Correctly() parsedCommand.Hierarchy!.ElementAt(1).Id.Should().Be("grp2"); parsedCommand.Command.Arguments.Should().HaveCount(2); - parsedCommand.Command.Arguments![0].Id.Should().Be("arg1"); - parsedCommand.Command.Arguments[1].Id.Should().Be("arg2"); + parsedCommand.Command.Arguments![0].CommandId.Should().Be("arg1"); + parsedCommand.Command.Arguments[1].CommandId.Should().Be("arg2"); parsedCommand.Command.Options.Should().HaveCount(6); parsedCommand.Command.Options!["opt1"].Value.Should().Be("val1"); @@ -139,8 +139,8 @@ public async Task No_Options_Processes_Correctly() parsedCommand.Should().NotBeNull(); parsedCommand!.Command.Id.Should().Be("grp1"); parsedCommand.Command.Arguments.Should().HaveCount(2); - parsedCommand.Command.Arguments![0].Id.Should().Be("arg1"); - parsedCommand.Command.Arguments[1].Id.Should().Be("arg2"); + parsedCommand.Command.Arguments![0].CommandId.Should().Be("arg1"); + parsedCommand.Command.Arguments[1].CommandId.Should().Be("arg2"); parsedCommand.Command.Options.Should().BeNull(); } diff --git a/test/OneImlx.Terminal.Tests/Commands/Routers/CommandRouterTests.cs b/test/OneImlx.Terminal.Tests/Commands/Routers/CommandRouterTests.cs index ecbfbedf..a1e39c58 100644 --- a/test/OneImlx.Terminal.Tests/Commands/Routers/CommandRouterTests.cs +++ b/test/OneImlx.Terminal.Tests/Commands/Routers/CommandRouterTests.cs @@ -13,7 +13,6 @@ using OneImlx.Terminal.Configuration.Options; using OneImlx.Terminal.Extensions; using OneImlx.Terminal.Mocks; -using OneImlx.Terminal.Runtime; using OneImlx.Terminal.Shared; using OneImlx.Test.FluentAssertions; using Xunit; diff --git a/test/OneImlx.Terminal.Tests/Commands/RunMethodTests.cs b/test/OneImlx.Terminal.Tests/Commands/RunMethodTests.cs index 5842076b..93e39dfa 100644 --- a/test/OneImlx.Terminal.Tests/Commands/RunMethodTests.cs +++ b/test/OneImlx.Terminal.Tests/Commands/RunMethodTests.cs @@ -7,7 +7,6 @@ using OneImlx.Terminal.Commands.Runners; using OneImlx.Terminal.Extensions; using OneImlx.Terminal.Mocks; -using OneImlx.Terminal.Runtime; using OneImlx.Terminal.Shared; using System; using System.Threading; @@ -31,31 +30,31 @@ public RunMethodTests() [Fact] public void Constructor_ThrowsArgumentNullException() { - Func act = () => new RunMethod(null!, "test-method"); + Func act = () => new RunMethodDescriptor(null!, "test-method"); act.Should().Throw().WithParameterName("id"); var methodInfo = typeof(MockRunnerWithBaseResult).GetMethod(nameof(MockRunnerWithBaseResult.TestMethodBase))!; - act = () => new RunMethod(null!, methodInfo); + act = () => new RunMethodDescriptor(null!, methodInfo); act.Should().Throw().WithParameterName("id"); - act = () => new RunMethod("test-id", methodName: null!); + act = () => new RunMethodDescriptor("test-id", methodName: null!); act.Should().Throw().WithParameterName("methodName"); - act = () => new RunMethod("test-id", methodInfo: null!); + act = () => new RunMethodDescriptor("test-id", methodInfo: null!); act.Should().Throw().WithParameterName("methodInfo"); } [Fact] public void Constructor_SetsPropertiesCorrectly() { - var runMethod = new RunMethod("test-id", "test-method"); - runMethod.Id.Should().Be("test-id"); + var runMethod = new RunMethodDescriptor("test-id", "test-method"); + runMethod.CommandId.Should().Be("test-id"); runMethod.MethodName.Should().Be("test-method"); runMethod.MethodInfo.Should().BeNull(); var methodInfo = typeof(MockRunnerWithBaseResult).GetMethod(nameof(MockRunnerWithBaseResult.TestMethodBase))!; - runMethod = new RunMethod("test-id", methodInfo); - runMethod.Id.Should().Be("test-id"); + runMethod = new RunMethodDescriptor("test-id", methodInfo); + runMethod.CommandId.Should().Be("test-id"); runMethod.MethodInfo.Should().BeSameAs(methodInfo); runMethod.MethodName.Should().Be(nameof(MockRunnerWithBaseResult.TestMethodBase)); } @@ -66,8 +65,8 @@ public async Task RunAsync_InvokesCorrectMethod_WhenMethodNameProvided_BaseResul MockRunnerWithBaseResult runner = new(); runner.MethodCalled.Should().BeFalse(); - var runMethod = new RunMethod("cmd-id", nameof(MockRunnerWithBaseResult.TestMethodBase)); - var result = await runMethod.DelegateRunAsync(runner, routerContext); + var runMethod = new RunMethodDescriptor("cmd-id", nameof(MockRunnerWithBaseResult.TestMethodBase)); + var result = await runMethod.RunAsync(runner, routerContext); runner.MethodCalled.Should().BeTrue(); result.Should().NotBeNull(); @@ -80,8 +79,8 @@ public async Task RunAsync_InvokesCorrectMethod_WhenMethodNameProvided_DerivedRe MockRunnerWithDerivedResult runner = new(); runner.MethodCalled.Should().BeFalse(); - var runMethod = new RunMethod("cmd-id", nameof(MockRunnerWithDerivedResult.TestMethodDerived)); - var result = await runMethod.DelegateRunAsync(runner, routerContext); + var runMethod = new RunMethodDescriptor("cmd-id", nameof(MockRunnerWithDerivedResult.TestMethodDerived)); + var result = await runMethod.RunAsync(runner, routerContext); runner.MethodCalled.Should().BeTrue(); result.Should().NotBeNull(); @@ -95,8 +94,8 @@ public async Task RunAsync_InvokesCorrectMethod_WhenMethodInfoProvided_BaseResul runner.MethodCalled.Should().BeFalse(); var methodInfo = typeof(MockRunnerWithBaseResult).GetMethod(nameof(MockRunnerWithBaseResult.TestMethodBase))!; - var runMethod = new RunMethod("cmd-id", methodInfo); - var result = await runMethod.DelegateRunAsync(runner, routerContext); + var runMethod = new RunMethodDescriptor("cmd-id", methodInfo); + var result = await runMethod.RunAsync(runner, routerContext); runner.MethodCalled.Should().BeTrue(); result.Should().NotBeNull(); @@ -110,8 +109,8 @@ public async Task RunAsync_InvokesCorrectMethod_WhenMethodInfoProvided_DerivedRe runner.MethodCalled.Should().BeFalse(); var methodInfo = typeof(MockRunnerWithDerivedResult).GetMethod(nameof(MockRunnerWithDerivedResult.TestMethodDerived))!; - var runMethod = new RunMethod("cmd-id", methodInfo); - var result = await runMethod.DelegateRunAsync(runner, routerContext); + var runMethod = new RunMethodDescriptor("cmd-id", methodInfo); + var result = await runMethod.RunAsync(runner, routerContext); runner.MethodCalled.Should().BeTrue(); result.Should().NotBeNull(); @@ -122,9 +121,9 @@ public async Task RunAsync_InvokesCorrectMethod_WhenMethodInfoProvided_DerivedRe public async Task RunAsync_Throws_WhenParsedCommandIsNull() { MockRunnerWithBaseResult runner = new(); - var runMethod = new RunMethod("cmd-id", nameof(MockRunnerWithBaseResult.TestMethodBase)); + var runMethod = new RunMethodDescriptor("cmd-id", nameof(MockRunnerWithBaseResult.TestMethodBase)); var contextWithoutParsedCommand = new CommandContext(new(Guid.NewGuid().ToString(), "test"), routingContext, []); - Func act = async () => await runMethod.DelegateRunAsync(runner, contextWithoutParsedCommand); + Func act = async () => await runMethod.RunAsync(runner, contextWithoutParsedCommand); await act.Should().ThrowAsync().WithMessage("The parsed command is missing in the context."); } @@ -132,8 +131,8 @@ public async Task RunAsync_Throws_WhenParsedCommandIsNull() public async Task RunAsync_Throws_WhenCommandIdDoesNotMatch() { MockRunnerWithBaseResult runner = new(); - var runMethod = new RunMethod("wrong-id", nameof(MockRunnerWithBaseResult.TestMethodBase)); - Func act = async () => await runMethod.DelegateRunAsync(runner, routerContext); + var runMethod = new RunMethodDescriptor("wrong-id", nameof(MockRunnerWithBaseResult.TestMethodBase)); + Func act = async () => await runMethod.RunAsync(runner, routerContext); await act.Should().ThrowAsync().WithMessage("The method's command is invalid. command=wrong-id"); } @@ -141,8 +140,8 @@ public async Task RunAsync_Throws_WhenCommandIdDoesNotMatch() public async Task RunAsync_Throws_WhenMethodNameNotFound() { MockRunnerWithBaseResult runner = new(); - var runMethod = new RunMethod("cmd-id", "NonExistentMethod"); - Func act = async () => await runMethod.DelegateRunAsync(runner, routerContext); + var runMethod = new RunMethodDescriptor("cmd-id", "NonExistentMethod"); + Func act = async () => await runMethod.RunAsync(runner, routerContext); await act.Should().ThrowAsync().WithMessage("No public run method found on the command. command=cmd-id, name=NonExistentMethod"); } @@ -150,13 +149,13 @@ public async Task RunAsync_Throws_WhenMethodNameNotFound() public async Task RunAsync_Throws_WhenBothMethodInfoAndMethodNameAreNull() { MockRunnerWithBaseResult runner = new(); - var runMethod = new RunMethod("cmd-id", "SomeMethod"); + var runMethod = new RunMethodDescriptor("cmd-id", "SomeMethod"); var runMethodType = runMethod.GetType(); var methodInfoField = runMethodType.GetField("k__BackingField", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); var methodNameField = runMethodType.GetField("k__BackingField", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); methodInfoField!.SetValue(runMethod, null); methodNameField!.SetValue(runMethod, null); - Func act = async () => await runMethod.DelegateRunAsync(runner, routerContext); + Func act = async () => await runMethod.RunAsync(runner, routerContext); await act.Should().ThrowAsync().WithMessage("The method name or method info is not registered. command=cmd-id"); } diff --git a/test/OneImlx.Terminal.Tests/Commands/Runners/CommandRunnerTests.cs b/test/OneImlx.Terminal.Tests/Commands/Runners/CommandRunnerTests.cs index 7daa3ea0..854a5eb9 100644 --- a/test/OneImlx.Terminal.Tests/Commands/Runners/CommandRunnerTests.cs +++ b/test/OneImlx.Terminal.Tests/Commands/Runners/CommandRunnerTests.cs @@ -2,17 +2,16 @@ // For license, terms, and data policies, go to: // https://terms.perpetualintelligence.com/articles/intro.html +using System; +using System.Threading; +using System.Threading.Tasks; using FluentAssertions; using OneImlx.Terminal.Commands.Handlers.Mocks; using OneImlx.Terminal.Commands.Parsers; using OneImlx.Terminal.Commands.Runners.Mocks; using OneImlx.Terminal.Extensions; using OneImlx.Terminal.Mocks; -using OneImlx.Terminal.Runtime; using OneImlx.Terminal.Shared; -using System; -using System.Threading; -using System.Threading.Tasks; using Xunit; namespace OneImlx.Terminal.Commands.Runners @@ -47,6 +46,8 @@ public async Task DelegateHelpShouldCallHelpAsync() public async Task DelegateRunShouldCallRunAsync() { Command command = new(new CommandDescriptor("id", "name", "desc", CommandTypes.Leaf)); + command.Descriptor.Runner = typeof(MockCommandRunner); + ParsedCommand extractedCommand = new(command, null); commandContext.SetParsedCommand(extractedCommand); diff --git a/test/OneImlx.Terminal.Tests/Commands/Runners/Mocks/MockDefaultCommandRunner.cs b/test/OneImlx.Terminal.Tests/Commands/Runners/Mocks/MockDefaultCommandRunner.cs index 060409be..02ca7d79 100644 --- a/test/OneImlx.Terminal.Tests/Commands/Runners/Mocks/MockDefaultCommandRunner.cs +++ b/test/OneImlx.Terminal.Tests/Commands/Runners/Mocks/MockDefaultCommandRunner.cs @@ -7,6 +7,7 @@ using System.Threading.Tasks; using OneImlx.Terminal.Commands.Handlers.Mocks; +using OneImlx.Terminal.Shared; namespace OneImlx.Terminal.Commands.Runners.Mocks { diff --git a/test/OneImlx.Terminal.Tests/Extensions/ICommandBuilderExtensionsTests.cs b/test/OneImlx.Terminal.Tests/Extensions/ICommandBuilderExtensionsTests.cs index f725b739..06a650e6 100644 --- a/test/OneImlx.Terminal.Tests/Extensions/ICommandBuilderExtensionsTests.cs +++ b/test/OneImlx.Terminal.Tests/Extensions/ICommandBuilderExtensionsTests.cs @@ -8,7 +8,6 @@ using OneImlx.Terminal.Commands; using OneImlx.Terminal.Hosting; using OneImlx.Terminal.Mocks; -using OneImlx.Terminal.Runtime; using OneImlx.Terminal.Shared; using System; using System.Linq; @@ -76,36 +75,32 @@ public void DefineOption_Adds_Std_DataType_Correctly() } [Fact] - public void DefineRunMethod_WithMethodName_Adds_Correctly() + public void RunMethod_WithMethodName_Adds_Correctly() { - IRunMethodBuilder runMethodBuilder = commandBuilder.DefineRunMethod("method1", "TestMethod"); + commandBuilder.RunMethod("method1", "TestMethod"); - runMethodBuilder.Services.Should().NotBeSameAs(commandBuilder.Services); - - ServiceDescriptor serviceDescriptor = runMethodBuilder.Services.First(static e => e.ServiceType.Equals(typeof(RunMethod))); + ServiceDescriptor serviceDescriptor = commandBuilder.Services.First(static e => e.ServiceType.Equals(typeof(RunMethodDescriptor))); serviceDescriptor!.Lifetime.Should().Be(ServiceLifetime.Singleton); serviceDescriptor.ImplementationType.Should().BeNull(); - RunMethod runMethod = (RunMethod)serviceDescriptor.ImplementationInstance!; - runMethod.Id.Should().Be("method1"); + RunMethodDescriptor runMethod = (RunMethodDescriptor)serviceDescriptor.ImplementationInstance!; + runMethod.CommandId.Should().Be("method1"); runMethod.MethodName.Should().Be("TestMethod"); runMethod.MethodInfo.Should().BeNull(); } [Fact] - public void DefineRunMethod_WithMethodInfo_Adds_Correctly() + public void RunMethod_WithMethodInfo_Adds_Correctly() { MethodInfo methodInfo = typeof(MockCommandRunner).GetMethod(nameof(MockCommandRunner.RunCommandAsync))!; - IRunMethodBuilder runMethodBuilder = commandBuilder.DefineRunMethod("method1", methodInfo); - - runMethodBuilder.Services.Should().NotBeSameAs(commandBuilder.Services); + commandBuilder.RunMethod("method1", methodInfo); - ServiceDescriptor serviceDescriptor = runMethodBuilder.Services.First(static e => e.ServiceType.Equals(typeof(RunMethod))); + ServiceDescriptor serviceDescriptor = commandBuilder.Services.First(static e => e.ServiceType.Equals(typeof(RunMethodDescriptor))); serviceDescriptor!.Lifetime.Should().Be(ServiceLifetime.Singleton); serviceDescriptor.ImplementationType.Should().BeNull(); - RunMethod runMethod = (RunMethod)serviceDescriptor.ImplementationInstance!; - runMethod.Id.Should().Be("method1"); + RunMethodDescriptor runMethod = (RunMethodDescriptor)serviceDescriptor.ImplementationInstance!; + runMethod.CommandId.Should().Be("method1"); runMethod.MethodInfo.Should().BeSameAs(methodInfo); runMethod.MethodName.Should().Be("RunCommandAsync"); } @@ -123,7 +118,7 @@ public void DefineArgument_Adds_Correctly() ArgumentDescriptor argument = (ArgumentDescriptor)serviceDescriptor.ImplementationInstance!; argument.Order.Should().Be(1); - argument.Id.Should().Be("arg1"); + argument.CommandId.Should().Be("arg1"); argument.DataType.Should().Be(nameof(String)); argument.Description.Should().Be("description1"); argument.Flags.Should().Be(BehaviorFlags.Required); diff --git a/test/OneImlx.Terminal.Tests/Extensions/ICommandContextExtensionsTests.cs b/test/OneImlx.Terminal.Tests/Extensions/ICommandContextExtensionsTests.cs index d444dd19..79c9bb7a 100644 --- a/test/OneImlx.Terminal.Tests/Extensions/ICommandContextExtensionsTests.cs +++ b/test/OneImlx.Terminal.Tests/Extensions/ICommandContextExtensionsTests.cs @@ -44,7 +44,7 @@ public void GetParsedCommand_ReturnsParsedCommand_WhenAvailable() public void GetParsedCommand_ThrowsTerminalException_WhenNotAvailable() { var mockContext = new Mock(); - mockContext.Setup(x => x.Properties).Returns(new Dictionary()); + mockContext.Setup(x => x.Properties).Returns([]); var act = () => mockContext.Object.GetParsedCommand(); act.Should().Throw().WithErrorCode(TerminalErrors.ServerError).WithErrorDescription("The parsed command is missing in the context."); } @@ -72,7 +72,7 @@ public void GetCommand_ReturnsCommand_WhenParsedCommandAvailable() public void GetCommand_ThrowsTerminalException_WhenParsedCommandNotAvailable() { var mockContext = new Mock(); - mockContext.Setup(x => x.Properties).Returns(new Dictionary()); + mockContext.Setup(x => x.Properties).Returns([]); var act = () => mockContext.Object.GetCommand(); act.Should().Throw().WithErrorCode(TerminalErrors.ServerError).WithErrorDescription("The parsed command is missing in the context."); } @@ -91,7 +91,7 @@ public void GetCommandResult_ReturnsCommandResult_WhenAvailable() public void GetCommandResult_ThrowsTerminalException_WhenNotAvailable() { var mockContext = new Mock(); - mockContext.Setup(x => x.Properties).Returns(new Dictionary()); + mockContext.Setup(x => x.Properties).Returns([]); var act = () => mockContext.Object.GetCommandResult(); act.Should().Throw().WithErrorCode(TerminalErrors.ServerError).WithErrorDescription("The command result is missing in the context."); } @@ -121,7 +121,7 @@ public void TryGetCommandResult_ReturnsTrue_WhenCommandResultAvailable() public void TryGetCommandResult_ReturnsFalse_WhenCommandResultNotAvailable() { var mockContext = new Mock(); - mockContext.Setup(x => x.Properties).Returns(new Dictionary()); + mockContext.Setup(x => x.Properties).Returns([]); mockContext.Object.TryGetCommandResult(out var outResult).Should().BeFalse(); } @@ -139,7 +139,7 @@ public void TryGetParsedCommand_ReturnsTrue_WhenParsedCommandAvailable() public void TryGetParsedCommand_ReturnsFalse_WhenParsedCommandNotAvailable() { var mockContext = new Mock(); - mockContext.Setup(x => x.Properties).Returns(new Dictionary()); + mockContext.Setup(x => x.Properties).Returns([]); mockContext.Object.TryGetParsedCommand(out var outResult).Should().BeFalse(); } diff --git a/test/OneImlx.Terminal.Tests/Extensions/IServiceCollectionExtensionsTests.cs b/test/OneImlx.Terminal.Tests/Extensions/IServiceCollectionExtensionsTests.cs index e1afabb2..2120882e 100644 --- a/test/OneImlx.Terminal.Tests/Extensions/IServiceCollectionExtensionsTests.cs +++ b/test/OneImlx.Terminal.Tests/Extensions/IServiceCollectionExtensionsTests.cs @@ -5,6 +5,8 @@ using FluentAssertions; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Options; +using OneImlx.Terminal.Configuration.Options; using OneImlx.Terminal.Commands; using OneImlx.Terminal.Commands.Checkers; using OneImlx.Terminal.Commands.Handlers; @@ -29,11 +31,16 @@ public void AddTerminalConsole_ShouldInitializeCorrectly() services.AddLogging(); var textHandler = new TerminalTextHandler(StringComparison.OrdinalIgnoreCase, Encoding.Unicode); + bool setupActionInvoked = false; - var builder = services.AddTerminalConsole(textHandler, static options => { }); + var builder = services.AddTerminalConsole(textHandler, options => { setupActionInvoked = true; }); builder.AddCommandContextFactory(); var provider = services.BuildServiceProvider(); + setupActionInvoked.Should().BeFalse(); + provider.GetRequiredService>().Value.Should().NotBeNull(); + setupActionInvoked.Should().BeTrue(); + // Text handler is special provider.GetService().Should().BeSameAs(textHandler); @@ -81,13 +88,18 @@ public void AddTerminalCli_ShouldInitializeCorrectly() services.AddLogging(); var textHandler = new TerminalTextHandler(StringComparison.OrdinalIgnoreCase, Encoding.Unicode); + bool setupActionInvoked = false; var builder = services.AddTerminalCli( textHandler, - static options => { }); + options => { setupActionInvoked = true; }); builder.AddCommandContextFactory(); var provider = services.BuildServiceProvider(); + setupActionInvoked.Should().BeFalse(); + provider.GetRequiredService>().Value.Should().NotBeNull(); + setupActionInvoked.Should().BeTrue(); + // Text handler provider.GetService().Should().BeSameAs(textHandler); @@ -123,14 +135,18 @@ public void AddTerminalClient_ShouldInitializeCorrectly() services.AddLogging(); var textHandler = new TerminalTextHandler(StringComparison.OrdinalIgnoreCase, Encoding.Unicode); + bool setupActionInvoked = false; services.AddTerminalClient( textHandler, - static options => { } - ); + options => { setupActionInvoked = true; }); var provider = services.BuildServiceProvider(); + setupActionInvoked.Should().BeFalse(); + provider.GetRequiredService>().Value.Should().NotBeNull(); + setupActionInvoked.Should().BeTrue(); + // Text handler provider.GetService().Should().BeSameAs(textHandler); @@ -166,14 +182,18 @@ public void AddTerminalServer_ShouldInitializeCorrectly() services.AddLogging(); var textHandler = new TerminalTextHandler(StringComparison.OrdinalIgnoreCase, Encoding.Unicode); + bool setupActionInvoked = false; services.AddTerminalServer( textHandler, - static options => { } - ); + options => { setupActionInvoked = true; }); var provider = services.BuildServiceProvider(); + setupActionInvoked.Should().BeFalse(); + provider.GetRequiredService>().Value.Should().NotBeNull(); + setupActionInvoked.Should().BeTrue(); + // Text handler provider.GetService().Should().BeSameAs(textHandler); diff --git a/test/OneImlx.Terminal.Tests/Hosting/ArgumentBuilderTests.cs b/test/OneImlx.Terminal.Tests/Hosting/ArgumentBuilderTests.cs index 6808b8dc..29477aaa 100644 --- a/test/OneImlx.Terminal.Tests/Hosting/ArgumentBuilderTests.cs +++ b/test/OneImlx.Terminal.Tests/Hosting/ArgumentBuilderTests.cs @@ -38,9 +38,9 @@ public void Build_Adds_ArgumentDescriptor_To_CommandDescriptor() ServiceProvider serviceProvider = commandBuilder.Services.BuildServiceProvider(); var argDescriptors = serviceProvider.GetServices(); argDescriptors.Count().Should().Be(3); - argDescriptors.Should().Contain(static x => x.Id == "arg1"); - argDescriptors.Should().Contain(static x => x.Id == "arg2"); - argDescriptors.Should().Contain(static x => x.Id == "arg3"); + argDescriptors.Should().Contain(static x => x.CommandId == "arg1"); + argDescriptors.Should().Contain(static x => x.CommandId == "arg2"); + argDescriptors.Should().Contain(static x => x.CommandId == "arg3"); } [Fact] diff --git a/test/OneImlx.Terminal.Tests/Hosting/CommandBuilderTests.cs b/test/OneImlx.Terminal.Tests/Hosting/CommandBuilderTests.cs index 66dd3918..bba284ac 100644 --- a/test/OneImlx.Terminal.Tests/Hosting/CommandBuilderTests.cs +++ b/test/OneImlx.Terminal.Tests/Hosting/CommandBuilderTests.cs @@ -146,12 +146,25 @@ public void Build_Adds_Command_With_RunMethods() { TerminalBuilder terminalBuilder = new(serviceCollection, new TerminalTextHandler(StringComparison.OrdinalIgnoreCase, Encoding.ASCII)); ICommandBuilder commandBuilder = terminalBuilder.DefineCommand("id1", "name1", "Command description", CommandTypes.CompositeGroup).Checker(); - commandBuilder.DefineRunMethod("method1", "TestMethod1").Add(); - commandBuilder.DefineRunMethod("method2", "TestMethod2").Add(); + commandBuilder.RunMethod("method1", "TestMethod1"); ITerminalBuilder tb = commandBuilder.Add(); ServiceProvider sp = tb.Services.BuildServiceProvider(); - var runMethods = sp.GetServices(); - runMethods.Count().Should().Be(2); + + var cmdDesc = sp.GetServices().First(c => c.Id == "id1"); + cmdDesc.RunMethod.Should().NotBeNull(); + cmdDesc.RunMethod.MethodName.Should().Be("TestMethod1"); + } + + [Fact] + public void Multiple_RunMethods_Throws() + { + TerminalBuilder terminalBuilder = new(serviceCollection, new TerminalTextHandler(StringComparison.OrdinalIgnoreCase, Encoding.ASCII)); + ICommandBuilder commandBuilder = terminalBuilder.DefineCommand("id1", "name1", "Command description", CommandTypes.CompositeGroup).Checker(); + commandBuilder.RunMethod("method1", "TestMethod1"); + commandBuilder.RunMethod("method2", "TestMethod2"); + + Action act = () => commandBuilder.Add(); + act.Should().Throw().WithErrorCode("invalid_command").WithErrorDescription("The command cannot have multiple run methods. command_type=3 command=id1"); } [Fact] diff --git a/test/OneImlx.Terminal.Tests/Hosting/RunMethodBuilderTests.cs b/test/OneImlx.Terminal.Tests/Hosting/RunMethodBuilderTests.cs deleted file mode 100644 index 514aa71a..00000000 --- a/test/OneImlx.Terminal.Tests/Hosting/RunMethodBuilderTests.cs +++ /dev/null @@ -1,104 +0,0 @@ -// Copyright © 2019-2026 Perpetual Intelligence L.L.C. All rights reserved. -// For license, terms, and data policies, go to: -// https://terms.perpetualintelligence.com/articles/intro.html - -using FluentAssertions; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; -using OneImlx.Terminal.Commands; -using OneImlx.Terminal.Extensions; -using OneImlx.Terminal.Mocks; -using OneImlx.Terminal.Runtime; -using OneImlx.Terminal.Shared; -using System; -using System.Linq; -using System.Text; -using Xunit; - -namespace OneImlx.Terminal.Hosting -{ - public class RunMethodBuilderTests - { - public RunMethodBuilderTests() - { - var hostBuilder = Host.CreateDefaultBuilder([]).ConfigureServices(ConfigureServicesDelegate); - host = hostBuilder.Build(); - } - - [Fact] - public void Build_Adds_RunMethod_To_CommandBuilder_ServiceCollection() - { - TerminalBuilder terminalBuilder = new(serviceCollection, new TerminalTextHandler(StringComparison.OrdinalIgnoreCase, Encoding.ASCII)); - ICommandBuilder commandBuilder = terminalBuilder.DefineCommand("cmd1", "name1", "Command description", CommandTypes.Leaf).Checker(); - ServiceDescriptor? serviceDescriptor = commandBuilder.Services.FirstOrDefault(static e => e.ServiceType.Equals(typeof(RunMethod))); - serviceDescriptor.Should().BeNull(); - - IRunMethodBuilder runMethodBuilder = commandBuilder.DefineRunMethod("method1", "TestMethod"); - serviceDescriptor = runMethodBuilder.Services.First(static e => e.ServiceType.Equals(typeof(RunMethod))); - serviceDescriptor!.Lifetime.Should().Be(ServiceLifetime.Singleton); - serviceDescriptor.ImplementationType.Should().BeNull(); - RunMethod instance = (RunMethod)serviceDescriptor.ImplementationInstance!; - instance.Id.Should().Be("method1"); - instance.MethodName.Should().Be("TestMethod"); - } - - [Fact] - public void Build_Returns_Same_CommandBuilder() - { - TerminalBuilder terminalBuilder = new(serviceCollection, new TerminalTextHandler(StringComparison.OrdinalIgnoreCase, Encoding.ASCII)); - ICommandBuilder commandBuilder = terminalBuilder.DefineCommand("cmd1", "name1", "Command description", CommandTypes.Leaf).Checker(); - IRunMethodBuilder runMethodBuilder = commandBuilder.DefineRunMethod("method1", "TestMethod"); - ICommandBuilder commandBuilderFromRunMethodBuilder = runMethodBuilder.Add(); - commandBuilder.Should().BeSameAs(commandBuilderFromRunMethodBuilder); - } - - [Fact] - public void NewBuilder_Returns_New_IServiceCollection() - { - TerminalBuilder terminalBuilder = new(serviceCollection, new TerminalTextHandler(StringComparison.OrdinalIgnoreCase, Encoding.ASCII)); - ICommandBuilder commandBuilder = terminalBuilder.DefineCommand("cmd1", "name1", "Command description", CommandTypes.Leaf).Checker(); - RunMethodBuilder runMethodBuilder = new(commandBuilder); - runMethodBuilder.Services.Should().NotBeSameAs(serviceCollection); - runMethodBuilder.Services.Should().NotBeSameAs(commandBuilder.Services); - } - - [Fact] - public void Build_Multiple_RunMethods_Adds_All() - { - TerminalBuilder terminalBuilder = new(serviceCollection, new TerminalTextHandler(StringComparison.OrdinalIgnoreCase, Encoding.ASCII)); - ICommandBuilder commandBuilder = terminalBuilder.DefineCommand("cmd1", "name1", "Command description", CommandTypes.Leaf).Checker(); - - IRunMethodBuilder runMethodBuilder = commandBuilder.DefineRunMethod("method1", "TestMethod1"); - runMethodBuilder.Services.AddSingleton(new RunMethod("method2", "TestMethod2")); - runMethodBuilder.Services.AddSingleton(new RunMethod("method3", "TestMethod3")); - - var serviceDescriptors = runMethodBuilder.Services.Where(static e => e.ServiceType.Equals(typeof(RunMethod))).ToList(); - serviceDescriptors.Should().HaveCount(3); - - RunMethod instance1 = (RunMethod)serviceDescriptors[0].ImplementationInstance!; - instance1.Id.Should().Be("method1"); - instance1.MethodName.Should().Be("TestMethod1"); - - RunMethod instance2 = (RunMethod)serviceDescriptors[1].ImplementationInstance!; - instance2.Id.Should().Be("method2"); - instance2.MethodName.Should().Be("TestMethod2"); - - RunMethod instance3 = (RunMethod)serviceDescriptors[2].ImplementationInstance!; - instance3.Id.Should().Be("method3"); - instance3.MethodName.Should().Be("TestMethod3"); - } - - ~RunMethodBuilderTests() - { - host.Dispose(); - } - - private void ConfigureServicesDelegate(IServiceCollection opt2) - { - serviceCollection = opt2; - } - - private readonly IHost host = null!; - private IServiceCollection serviceCollection = null!; - } -} \ No newline at end of file diff --git a/test/OneImlx.Terminal.Tests/Mocks/MockCommandChecker.cs b/test/OneImlx.Terminal.Tests/Mocks/MockCommandChecker.cs index d4459919..0e0c496b 100644 --- a/test/OneImlx.Terminal.Tests/Mocks/MockCommandChecker.cs +++ b/test/OneImlx.Terminal.Tests/Mocks/MockCommandChecker.cs @@ -5,8 +5,8 @@ https://terms.perpetualintelligence.com/articles/intro.html */ -using OneImlx.Terminal.Commands; using OneImlx.Terminal.Commands.Checkers; +using OneImlx.Terminal.Shared; using System.Threading.Tasks; namespace OneImlx.Terminal.Mocks diff --git a/test/OneImlx.Terminal.Tests/Mocks/MockCommandHandler.cs b/test/OneImlx.Terminal.Tests/Mocks/MockCommandHandler.cs index 5d792a49..4aed8865 100644 --- a/test/OneImlx.Terminal.Tests/Mocks/MockCommandHandler.cs +++ b/test/OneImlx.Terminal.Tests/Mocks/MockCommandHandler.cs @@ -5,8 +5,8 @@ https://terms.perpetualintelligence.com/articles/intro.html */ -using OneImlx.Terminal.Commands; using OneImlx.Terminal.Commands.Handlers; +using OneImlx.Terminal.Shared; using System.Threading.Tasks; namespace OneImlx.Terminal.Mocks diff --git a/test/OneImlx.Terminal.Tests/Mocks/MockCommandResolver.cs b/test/OneImlx.Terminal.Tests/Mocks/MockCommandResolver.cs index d4de6c87..e837a747 100644 --- a/test/OneImlx.Terminal.Tests/Mocks/MockCommandResolver.cs +++ b/test/OneImlx.Terminal.Tests/Mocks/MockCommandResolver.cs @@ -2,12 +2,12 @@ // For license, terms, and data policies, go to: // https://terms.perpetualintelligence.com/articles/intro.html +using System.Reflection; using OneImlx.Terminal.Commands; using OneImlx.Terminal.Commands.Checkers; using OneImlx.Terminal.Commands.Handlers; using OneImlx.Terminal.Commands.Handlers.Mocks; using OneImlx.Terminal.Commands.Runners; -using System.Reflection; namespace OneImlx.Terminal.Mocks { @@ -25,14 +25,10 @@ internal class MockCommandResolver : ICommandResolver public IDelegateCommandRunner? ReturnedRunner { get; private set; } - public RunMethod? ReturnedRunMethod { get; private set; } - public ICommandChecker? ReturnThisChecker { get; set; } public IDelegateCommandRunner? ReturnThisRunner { get; set; } - public RunMethod? ReturnThisRunMethod { get; set; } = null; - public ICommandChecker ResolveCommandChecker(CommandDescriptor commandDescriptor) { ResolveCheckerCalled = true; @@ -63,22 +59,5 @@ public IDelegateCommandRunner ResolveCommandRunner(CommandDescriptor commandDesc return ReturnedRunner; } - - public RunMethod ResolveCommandRunMethod(CommandDescriptor commandDescriptor) - { - ResolveRunnerMethodCalled = true; - - if (ReturnThisRunMethod != null) - { - ReturnedRunMethod = ReturnThisRunMethod; - } - else - { - MethodInfo methodInfo = typeof(MockCommandRunnerInner).GetMethod(nameof(MockCommandRunnerInner.RunCommandAsync))!; - ReturnedRunMethod = new RunMethod(commandDescriptor.Id, methodInfo); - } - - return ReturnedRunMethod; - } } } \ No newline at end of file diff --git a/test/OneImlx.Terminal.Tests/Mocks/MockCommandRunner.cs b/test/OneImlx.Terminal.Tests/Mocks/MockCommandRunner.cs index 345e808c..226e3b60 100644 --- a/test/OneImlx.Terminal.Tests/Mocks/MockCommandRunner.cs +++ b/test/OneImlx.Terminal.Tests/Mocks/MockCommandRunner.cs @@ -6,8 +6,8 @@ */ using System.Threading.Tasks; -using OneImlx.Terminal.Commands; using OneImlx.Terminal.Commands.Runners; +using OneImlx.Terminal.Shared; namespace OneImlx.Terminal.Mocks { diff --git a/test/OneImlx.Terminal.Tests/Mocks/MockCommands.cs b/test/OneImlx.Terminal.Tests/Mocks/MockCommands.cs index 811b266e..b3359601 100644 --- a/test/OneImlx.Terminal.Tests/Mocks/MockCommands.cs +++ b/test/OneImlx.Terminal.Tests/Mocks/MockCommands.cs @@ -63,6 +63,13 @@ static MockCommands() new("चार", nameof(Double), "चौथा तर्क", BehaviorFlags.None, "चारहै"), ]); + TestArgumentDescriptors = new(unicodeTextHandler, + [ + new ArgumentDescriptor(1, "arg1", nameof(String), "First positional argument", BehaviorFlags.None), + new ArgumentDescriptor(2, "arg2", nameof(Int32), "Second positional argument", BehaviorFlags.Required), + new ArgumentDescriptor(3, "arg3", nameof(Boolean), "Third positional argument", BehaviorFlags.None), + ]); + Commands = new(unicodeTextHandler, [ @@ -180,9 +187,9 @@ static MockCommands() ]); } - public static Tuple NewCommandDefinition(string id, string name, string desc, int commandType, OptionDescriptors? args = null, Type? checker = null, Type? runner = null, Options? options = null) + public static Tuple NewCommandDefinition(string id, string name, string desc, int commandType, OptionDescriptors? args = null, Type? checker = null, Type? runner = null, Options? options = null, ArgumentDescriptors? argumentDescriptors = null) { - var cmd1 = new CommandDescriptor(id, name, desc, commandType, optionDescriptors: args) + var cmd1 = new CommandDescriptor(id, name, desc, commandType, argumentDescriptors: argumentDescriptors, optionDescriptors: args) { Checker = checker, Runner = runner, @@ -191,6 +198,7 @@ public static Tuple NewCommandDefinition(string id, return new Tuple(cmd1, new Command(cmd1, options: options)); } + public static ArgumentDescriptors TestArgumentDescriptors; public static CommandDescriptors Commands; public static CommandDescriptors GroupedCommands; public static CommandDescriptors GroupedOptionsCommands; diff --git a/test/OneImlx.Terminal.Tests/Mocks/MockRoutingContext.cs b/test/OneImlx.Terminal.Tests/Mocks/MockRoutingContext.cs index 0a476c3e..b61e959b 100644 --- a/test/OneImlx.Terminal.Tests/Mocks/MockRoutingContext.cs +++ b/test/OneImlx.Terminal.Tests/Mocks/MockRoutingContext.cs @@ -5,7 +5,7 @@ https://terms.perpetualintelligence.com/articles/intro.html */ -using OneImlx.Terminal.Runtime; +using OneImlx.Terminal.Shared; using System.Collections.Generic; using System.Threading; diff --git a/test/OneImlx.Terminal.Tests/Mocks/MockTerminalRouterContext.cs b/test/OneImlx.Terminal.Tests/Mocks/MockTerminalRouterContext.cs index cf4334d7..89189fa6 100644 --- a/test/OneImlx.Terminal.Tests/Mocks/MockTerminalRouterContext.cs +++ b/test/OneImlx.Terminal.Tests/Mocks/MockTerminalRouterContext.cs @@ -5,7 +5,7 @@ https://terms.perpetualintelligence.com/articles/intro.html */ -using OneImlx.Terminal.Runtime; +using OneImlx.Terminal.Shared; using System.Collections.Generic; using System.Threading; diff --git a/test/OneImlx.Terminal.Tests/Runtime/TerminalBytesParserTests.cs b/test/OneImlx.Terminal.Tests/Runtime/TerminalBytesParserTests.cs index e4e9ec0a..d3e1a6fc 100644 --- a/test/OneImlx.Terminal.Tests/Runtime/TerminalBytesParserTests.cs +++ b/test/OneImlx.Terminal.Tests/Runtime/TerminalBytesParserTests.cs @@ -247,9 +247,9 @@ public void Split_HandlesLargeSegments() { // Arrange // Use delimiter 0xFF and ensure test data uses values 0x00-0xFE to avoid accidental splits - byte[] segment1 = Enumerable.Range(0, 1000).Select(x => (byte)(x % 255)).ToArray(); - byte[] segment2 = Enumerable.Range(0, 2000).Select(x => (byte)(x % 255)).ToArray(); - byte[] source = segment1.Concat(new byte[] { 0xFF }).Concat(segment2).ToArray(); + byte[] segment1 = [.. Enumerable.Range(0, 1000).Select(x => (byte)(x % 255))]; + byte[] segment2 = [.. Enumerable.Range(0, 2000).Select(x => (byte)(x % 255))]; + byte[] source = [.. segment1, .. new byte[] { 0xFF }, .. segment2]; byte delimiter = 0xFF; // Act @@ -271,9 +271,7 @@ public void Split_HandlesManySmallSegments() .Select(i => Enumerable.Range(0, 10).Select(j => (byte)(i + j)).ToArray()) .ToArray(); - byte[] source = segments - .SelectMany(s => s.Concat(new byte[] { 0xFF })) - .ToArray(); + byte[] source = [.. segments.SelectMany(s => s.Concat(new byte[] { 0xFF }))]; byte delimiter = 0xFF; // Act @@ -297,9 +295,7 @@ public void Split_HandlesManySmallSegments_IgnoreEmpty() .Select(i => Enumerable.Range(0, 10).Select(j => (byte)(i + j)).ToArray()) .ToArray(); - byte[] source = segments - .SelectMany(s => s.Concat(new byte[] { 0xFF })) - .ToArray(); + byte[] source = [.. segments.SelectMany(s => s.Concat(new byte[] { 0xFF }))]; byte delimiter = 0xFF; // Act @@ -361,12 +357,7 @@ public void Split_HandlesStreamingScenario_PartialMessage() byte[] jsonMessage2 = System.Text.Encoding.UTF8.GetBytes("{\"id\":2}"); byte[] jsonMessage3Partial = System.Text.Encoding.UTF8.GetBytes("{\"id\":3,\"da"); // Incomplete - byte[] source = jsonMessage1 - .Concat(new byte[] { 0x1E }) - .Concat(jsonMessage2) - .Concat(new byte[] { 0x1E }) - .Concat(jsonMessage3Partial) - .ToArray(); + byte[] source = [.. jsonMessage1, .. new byte[] { 0x1E }, .. jsonMessage2, .. new byte[] { 0x1E }, .. jsonMessage3Partial]; byte delimiter = 0x1E; // Act @@ -387,11 +378,7 @@ public void Split_HandlesStreamingScenario_CompleteMessages() byte[] jsonMessage1 = System.Text.Encoding.UTF8.GetBytes("{\"id\":1}"); byte[] jsonMessage2 = System.Text.Encoding.UTF8.GetBytes("{\"id\":2}"); - byte[] source = jsonMessage1 - .Concat(new byte[] { 0x1E }) - .Concat(jsonMessage2) - .Concat(new byte[] { 0x1E }) - .ToArray(); + byte[] source = [.. jsonMessage1, .. new byte[] { 0x1E }, .. jsonMessage2, .. new byte[] { 0x1E }]; byte delimiter = 0x1E; // Act @@ -435,6 +422,7 @@ public void Split_ThrowsArgumentException_WhenSourceIsNull() } [Fact] + [Trait("Category", "Performance")] public void Split_Performance_SmallBuffer_10KB() { // Arrange @@ -465,6 +453,7 @@ public void Split_Performance_SmallBuffer_10KB() } [Fact] + [Trait("Category", "Performance")] public void Split_Performance_MediumBuffer_100KB() { // Arrange @@ -495,6 +484,7 @@ public void Split_Performance_MediumBuffer_100KB() } [Fact] + [Trait("Category", "Performance")] public void Split_Performance_LargeBuffer_1MB() { // Arrange @@ -525,6 +515,7 @@ public void Split_Performance_LargeBuffer_1MB() } [Fact] + [Trait("Category", "Performance")] public void Split_Performance_ManySmallSegments() { // Arrange @@ -555,6 +546,7 @@ public void Split_Performance_ManySmallSegments() } [Fact] + [Trait("Category", "Performance")] public void Split_Performance_NoDelimiters() { // Arrange @@ -586,6 +578,7 @@ public void Split_Performance_NoDelimiters() } [Fact] + [Trait("Category", "Performance")] public void Split_Performance_IgnoreEmptyVsKeepEmpty() { // Arrange @@ -627,7 +620,7 @@ public void Split_Performance_IgnoreEmptyVsKeepEmpty() avgKeepEmpty.Should().BeLessThan(0.5); } - private byte[] CreateTestBuffer(int totalSize, int delimiterCount) + private static byte[] CreateTestBuffer(int totalSize, int delimiterCount) { byte[] buffer = new byte[totalSize]; diff --git a/test/OneImlx.Terminal.Tests/Runtime/TerminalConsoleExceptionHandlerTests.cs b/test/OneImlx.Terminal.Tests/Runtime/TerminalConsoleExceptionHandlerTests.cs new file mode 100644 index 00000000..810b7e9b --- /dev/null +++ b/test/OneImlx.Terminal.Tests/Runtime/TerminalConsoleExceptionHandlerTests.cs @@ -0,0 +1,81 @@ +// Copyright © 2019-2026 Perpetual Intelligence L.L.C. All rights reserved. +// For license, terms, and data policies, go to: +// https://terms.perpetualintelligence.com/articles/intro.html + +using Moq; +using OneImlx.Shared.Infrastructure; +using OneImlx.Terminal.Shared; +using System; +using System.Threading.Tasks; +using Xunit; + +namespace OneImlx.Terminal.Runtime +{ + public class TerminalConsoleExceptionHandlerTests + { + public TerminalConsoleExceptionHandlerTests() + { + _mockConsole = new Mock(); + _handler = new TerminalConsoleExceptionHandler(_mockConsole.Object); + } + + [Fact] + public async Task HandleExceptionAsync_TerminalException_WithArgs_WritesErrorDescription() + { + var ex = new TerminalException(new Error("test_error", "Something went wrong with {0}", "details")); + + await _handler.HandleExceptionAsync(new TerminalExceptionHandlerContext(ex)); + + _mockConsole.Verify(c => c.WriteLineColorAsync(ConsoleColor.Red, "Something went wrong with {0}", It.IsAny()), Times.Once); + } + + [Fact] + public async Task HandleExceptionAsync_TerminalException_WithNullArgs_WritesErrorDescription() + { + var ex = new TerminalException(new Error("test_error", "No args error")); + + await _handler.HandleExceptionAsync(new TerminalExceptionHandlerContext(ex)); + + _mockConsole.Verify(c => c.WriteLineColorAsync(ConsoleColor.Red, "No args error", It.IsAny()), Times.Once); + } + + [Fact] + public async Task HandleExceptionAsync_OperationCanceledException_WithRequest_WritesRequestInfo() + { + var request = new CommandRequest("req1", "test command"); + + await _handler.HandleExceptionAsync(new TerminalExceptionHandlerContext(new OperationCanceledException(), request)); + + _mockConsole.Verify(c => c.WriteLineColorAsync(ConsoleColor.Red, "The terminal request was canceled. request={0} command={1}", "req1", "test command"), Times.Once); + } + + [Fact] + public async Task HandleExceptionAsync_OperationCanceledException_WithoutRequest_WritesGenericMessage() + { + await _handler.HandleExceptionAsync(new TerminalExceptionHandlerContext(new OperationCanceledException())); + + _mockConsole.Verify(c => c.WriteLineColorAsync(ConsoleColor.Red, "The terminal request was canceled."), Times.Once); + } + + [Fact] + public async Task HandleExceptionAsync_GenericException_WithRequest_WritesRequestInfo() + { + var request = new CommandRequest("req2", "run cmd"); + + await _handler.HandleExceptionAsync(new TerminalExceptionHandlerContext(new InvalidOperationException("something failed"), request)); + + _mockConsole.Verify(c => c.WriteLineColorAsync(ConsoleColor.Red, "The terminal request failed. request={0} command={1} info={2}", "req2", "run cmd", "something failed"), Times.Once); + } + + [Fact] + public async Task HandleExceptionAsync_GenericException_WithoutRequest_WritesGenericMessage() + { + await _handler.HandleExceptionAsync(new TerminalExceptionHandlerContext(new InvalidOperationException("generic failure"))); + + _mockConsole.Verify(c => c.WriteLineColorAsync(ConsoleColor.Red, "The terminal request failed. info={0}", "generic failure"), Times.Once); + } + + private readonly Mock _mockConsole; + private readonly TerminalConsoleExceptionHandler _handler; + } +} \ No newline at end of file diff --git a/test/OneImlx.Terminal.Tests/Runtime/TerminalConsoleHelpProviderTests.cs b/test/OneImlx.Terminal.Tests/Runtime/TerminalConsoleHelpProviderTests.cs new file mode 100644 index 00000000..3fb230c4 --- /dev/null +++ b/test/OneImlx.Terminal.Tests/Runtime/TerminalConsoleHelpProviderTests.cs @@ -0,0 +1,154 @@ +// Copyright © 2019-2026 Perpetual Intelligence L.L.C. All rights reserved. +// For license, terms, and data policies, go to: +// https://terms.perpetualintelligence.com/articles/intro.html + +using FluentAssertions; +using OneImlx.Terminal.Commands; +using OneImlx.Terminal.Configuration.Options; +using OneImlx.Terminal.Mocks; +using OneImlx.Terminal.Shared; +using System; +using System.Text; +using System.Threading.Tasks; +using Xunit; + +namespace OneImlx.Terminal.Runtime +{ + public class TerminalConsoleHelpProviderTests + { + public TerminalConsoleHelpProviderTests() + { + _console = new MockTerminalConsole(); + _options = new TerminalOptions + { + Parser = new ParserOptions { OptionPrefix = '-' } + }; + _provider = new TerminalConsoleHelpProvider(_options, _console); + } + + [Fact] + public async Task ProvideHelpAsync_CommandOnly_WritesExactMessages() + { + var (_, command) = MockCommands.NewCommandDefinition("id4", "name4", "desc4", CommandTypes.Leaf); + + await _provider.ProvideHelpAsync(new TerminalHelpProviderContext(command)); + + _console.Messages.Should().HaveCount(3); + _console.Messages[0].Should().Be("Command:"); + _console.Messages[1].Should().Be(" id4 (name4) 4"); + _console.Messages[2].Should().Be(" desc4"); + } + + [Fact] + public async Task ProvideHelpAsync_OptionWithoutAlias_WritesExactMessages() + { + var textHandler = new TerminalTextHandler(StringComparison.OrdinalIgnoreCase, Encoding.Unicode); + var optionDescriptors = new OptionDescriptors(textHandler, + [ + new OptionDescriptor("verbose", nameof(Boolean), "Enable verbose output", BehaviorFlags.None) + ]); + var (_, command) = MockCommands.NewCommandDefinition("id1", "name1", "desc1", CommandTypes.Leaf, optionDescriptors); + + await _provider.ProvideHelpAsync(new TerminalHelpProviderContext(command)); + + _console.Messages.Should().HaveCount(6); + _console.Messages[0].Should().Be("Command:"); + _console.Messages[1].Should().Be(" id1 (name1) 4"); + _console.Messages[2].Should().Be(" desc1"); + _console.Messages[3].Should().Be("Options:"); + _console.Messages[4].Should().Be(" --verbose "); + _console.Messages[5].Should().Be(" Enable verbose output"); + } + + [Fact] + public async Task ProvideHelpAsync_OptionWithAlias_WritesExactMessages() + { + var textHandler = new TerminalTextHandler(StringComparison.OrdinalIgnoreCase, Encoding.Unicode); + var optionDescriptors = new OptionDescriptors(textHandler, + [ + new OptionDescriptor("verbose", nameof(Boolean), "Enable verbose output", BehaviorFlags.None, "v") + ]); + var (_, command) = MockCommands.NewCommandDefinition("id1", "name1", "desc1", CommandTypes.Leaf, optionDescriptors); + + await _provider.ProvideHelpAsync(new TerminalHelpProviderContext(command)); + + _console.Messages.Should().HaveCount(6); + _console.Messages[0].Should().Be("Command:"); + _console.Messages[1].Should().Be(" id1 (name1) 4"); + _console.Messages[2].Should().Be(" desc1"); + _console.Messages[3].Should().Be("Options:"); + _console.Messages[4].Should().Be(" --verbose, -v "); + _console.Messages[5].Should().Be(" Enable verbose output"); + } + + [Fact] + public async Task ProvideHelpAsync_NoOptionsNoArguments_WritesOnlyCommandMessages() + { + var (_, command) = MockCommands.NewCommandDefinition("id4", "name4", "desc4", CommandTypes.Leaf); + + await _provider.ProvideHelpAsync(new TerminalHelpProviderContext(command)); + + _console.Messages.Should().HaveCount(3); + _console.Messages.Should().NotContain("Options:"); + _console.Messages.Should().NotContain("Arguments:"); + } + + [Fact] + public async Task ProvideHelpAsync_ArgumentsOnly_WritesExactMessages() + { + var textHandler = new TerminalTextHandler(StringComparison.OrdinalIgnoreCase, Encoding.Unicode); + var argumentDescriptors = new ArgumentDescriptors(textHandler, + [ + new ArgumentDescriptor(1, "file", nameof(String), "The file path", BehaviorFlags.Required), + new ArgumentDescriptor(2, "count", nameof(Int32), "The item count", BehaviorFlags.None), + ]); + var (_, command) = MockCommands.NewCommandDefinition("id1", "name1", "desc1", CommandTypes.Leaf, + argumentDescriptors: argumentDescriptors); + + await _provider.ProvideHelpAsync(new TerminalHelpProviderContext(command)); + + _console.Messages.Should().HaveCount(8); + _console.Messages[0].Should().Be("Command:"); + _console.Messages[1].Should().Be(" id1 (name1) 4"); + _console.Messages[2].Should().Be(" desc1"); + _console.Messages[3].Should().Be("Arguments:"); + _console.Messages[4].Should().Be(" file "); + _console.Messages[5].Should().Be(" The file path"); + _console.Messages[6].Should().Be(" count "); + _console.Messages[7].Should().Be(" The item count"); + } + + [Fact] + public async Task ProvideHelpAsync_ArgumentsAndOptions_WritesExactMessages() + { + var textHandler = new TerminalTextHandler(StringComparison.OrdinalIgnoreCase, Encoding.Unicode); + var argumentDescriptors = new ArgumentDescriptors(textHandler, + [ + new ArgumentDescriptor(1, "source", nameof(String), "The source path", BehaviorFlags.Required), + ]); + var optionDescriptors = new OptionDescriptors(textHandler, + [ + new OptionDescriptor("verbose", nameof(Boolean), "Enable verbose output", BehaviorFlags.None, "v"), + ]); + var (_, command) = MockCommands.NewCommandDefinition("id1", "name1", "desc1", CommandTypes.Leaf, + args: optionDescriptors, argumentDescriptors: argumentDescriptors); + + await _provider.ProvideHelpAsync(new TerminalHelpProviderContext(command)); + + _console.Messages.Should().HaveCount(9); + _console.Messages[0].Should().Be("Command:"); + _console.Messages[1].Should().Be(" id1 (name1) 4"); + _console.Messages[2].Should().Be(" desc1"); + _console.Messages[3].Should().Be("Arguments:"); + _console.Messages[4].Should().Be(" source "); + _console.Messages[5].Should().Be(" The source path"); + _console.Messages[6].Should().Be("Options:"); + _console.Messages[7].Should().Be(" --verbose, -v "); + _console.Messages[8].Should().Be(" Enable verbose output"); + } + + private readonly MockTerminalConsole _console; + private readonly TerminalOptions _options; + private readonly TerminalConsoleHelpProvider _provider; + } +} \ No newline at end of file diff --git a/test/OneImlx.Terminal.Tests/Runtime/TerminalLoggerExceptionHandlerTests.cs b/test/OneImlx.Terminal.Tests/Runtime/TerminalLoggerExceptionHandlerTests.cs new file mode 100644 index 00000000..73d6384a --- /dev/null +++ b/test/OneImlx.Terminal.Tests/Runtime/TerminalLoggerExceptionHandlerTests.cs @@ -0,0 +1,136 @@ +// Copyright © 2019-2026 Perpetual Intelligence L.L.C. All rights reserved. +// For license, terms, and data policies, go to: +// https://terms.perpetualintelligence.com/articles/intro.html + +using FluentAssertions; +using Microsoft.Extensions.Logging; +using OneImlx.Terminal.Shared; +using OneImlx.Terminal.Testing.Mocks; +using System; +using System.Threading.Tasks; +using Xunit; + +namespace OneImlx.Terminal.Runtime +{ + public class TerminalLoggerExceptionHandlerTests + { + public TerminalLoggerExceptionHandlerTests() + { + _loggerFactory = new MockListLoggerFactory(); + _handler = new TerminalLoggerExceptionHandler(((ILoggerFactory)_loggerFactory).CreateLogger()); + } + + [Fact] + public async Task HandleExceptionAsync_TerminalException_NoRequest_WritesExactMessages() + { + var context = new TerminalExceptionHandlerContext( + new TerminalException("err", "Something went wrong.")); + + await _handler.HandleExceptionAsync(context); + + _loggerFactory.AllLogMessages.Should().HaveCount(2); + _loggerFactory.AllLogMessages[0].Should().Be("Handle exception. request="); + _loggerFactory.AllLogMessages[1].Should().Be("Something went wrong."); + } + + [Fact] + public async Task HandleExceptionAsync_TerminalException_WithRequest_WritesExactMessages() + { + var context = new TerminalExceptionHandlerContext( + new TerminalException("err", "Something went wrong."), + new CommandRequest("req-1", "run cmd")); + + await _handler.HandleExceptionAsync(context); + + _loggerFactory.AllLogMessages.Should().HaveCount(2); + _loggerFactory.AllLogMessages[0].Should().Be("Handle exception. request=req-1"); + _loggerFactory.AllLogMessages[1].Should().Be("Something went wrong."); + } + + [Fact] + public async Task HandleExceptionAsync_TerminalException_WithArgs_WritesExactMessages() + { + var context = new TerminalExceptionHandlerContext( + new TerminalException("err", "Value {0} is invalid for {1}.", "foo", "bar")); + + await _handler.HandleExceptionAsync(context); + + _loggerFactory.AllLogMessages.Should().HaveCount(2); + _loggerFactory.AllLogMessages[0].Should().Be("Handle exception. request="); + _loggerFactory.AllLogMessages[1].Should().Be("Value foo is invalid for bar."); + } + + [Fact] + public async Task HandleExceptionAsync_OperationCanceled_NoRequest_WritesExactMessages() + { + var context = new TerminalExceptionHandlerContext(new OperationCanceledException()); + + await _handler.HandleExceptionAsync(context); + + _loggerFactory.AllLogMessages.Should().HaveCount(2); + _loggerFactory.AllLogMessages[0].Should().Be("Handle exception. request="); + _loggerFactory.AllLogMessages[1].Should().Be("The request was canceled."); + } + + [Fact] + public async Task HandleExceptionAsync_OperationCanceled_WithRequest_WritesExactMessages() + { + var context = new TerminalExceptionHandlerContext( + new OperationCanceledException(), + new CommandRequest("req-2", "run cmd")); + + await _handler.HandleExceptionAsync(context); + + _loggerFactory.AllLogMessages.Should().HaveCount(2); + _loggerFactory.AllLogMessages[0].Should().Be("Handle exception. request=req-2"); + _loggerFactory.AllLogMessages[1].Should().Be("The request was canceled. request=req-2 command=run cmd"); + } + + [Fact] + public async Task HandleExceptionAsync_GenericException_NoRequest_WritesExactMessages() + { + var context = new TerminalExceptionHandlerContext(new InvalidOperationException("Something bad.")); + + await _handler.HandleExceptionAsync(context); + + _loggerFactory.AllLogMessages.Should().HaveCount(2); + _loggerFactory.AllLogMessages[0].Should().Be("Handle exception. request="); + _loggerFactory.AllLogMessages[1].Should().Be("The request failed."); + } + + [Fact] + public async Task HandleExceptionAsync_GenericException_WithRequest_WritesExactMessages() + { + var context = new TerminalExceptionHandlerContext( + new InvalidOperationException("Something bad."), + new CommandRequest("req-4", "run cmd")); + + await _handler.HandleExceptionAsync(context); + + _loggerFactory.AllLogMessages.Should().HaveCount(2); + _loggerFactory.AllLogMessages[0].Should().Be("Handle exception. request=req-4"); + _loggerFactory.AllLogMessages[1].Should().Be("The request failed. request=req-4 command=run cmd info=Something bad."); + } + + [Fact] + public async Task HandleExceptionAsync_NullException_Throws() + { + Func act = () => _handler.HandleExceptionAsync(new TerminalExceptionHandlerContext(null!)); + await act.Should().ThrowAsync(); + } + + [Fact] + public async Task HandleExceptionAsync_Always_ReturnsCompletedTask() + { + var context = new TerminalExceptionHandlerContext(new Exception("test")); + + Task result = _handler.HandleExceptionAsync(context); + + result.IsCompleted.Should().BeTrue(); + await result; + } + + private readonly MockListLoggerFactory _loggerFactory; + private readonly TerminalLoggerExceptionHandler _handler; + } +} \ No newline at end of file diff --git a/test/OneImlx.Terminal.Tests/Runtime/TerminalLoggerHelpProviderTests.cs b/test/OneImlx.Terminal.Tests/Runtime/TerminalLoggerHelpProviderTests.cs new file mode 100644 index 00000000..e29c276a --- /dev/null +++ b/test/OneImlx.Terminal.Tests/Runtime/TerminalLoggerHelpProviderTests.cs @@ -0,0 +1,156 @@ +// Copyright © 2019-2026 Perpetual Intelligence L.L.C. All rights reserved. +// For license, terms, and data policies, go to: +// https://terms.perpetualintelligence.com/articles/intro.html + +using System; +using System.Text; +using System.Threading.Tasks; +using FluentAssertions; +using Microsoft.Extensions.Logging; +using OneImlx.Terminal.Commands; +using OneImlx.Terminal.Configuration.Options; +using OneImlx.Terminal.Mocks; +using OneImlx.Terminal.Shared; +using OneImlx.Terminal.Testing.Mocks; +using Xunit; + +namespace OneImlx.Terminal.Runtime +{ + public class TerminalLoggerHelpProviderTests + { + public TerminalLoggerHelpProviderTests() + { + _loggerFactory = new MockListLoggerFactory(); + _options = new TerminalOptions + { + Parser = new ParserOptions { OptionPrefix = '-' } + }; + _provider = new TerminalLoggerHelpProvider(_options, ((ILoggerFactory)_loggerFactory).CreateLogger()); + } + + [Fact] + public async Task ProvideHelpAsync_CommandOnly_WritesExactMessages() + { + var (_, command) = MockCommands.NewCommandDefinition("id4", "name4", "desc4", CommandTypes.Leaf); + + await _provider.ProvideHelpAsync(new TerminalHelpProviderContext(command)); + + _loggerFactory.AllLogMessages.Should().HaveCount(3); + _loggerFactory.AllLogMessages[0].Should().Be("Command:"); + _loggerFactory.AllLogMessages[1].Should().Be(" id4 (name4) 4"); + _loggerFactory.AllLogMessages[2].Should().Be(" desc4"); + } + + [Fact] + public async Task ProvideHelpAsync_OptionWithoutAlias_WritesExactMessages() + { + var textHandler = new TerminalTextHandler(StringComparison.OrdinalIgnoreCase, Encoding.Unicode); + var optionDescriptors = new OptionDescriptors(textHandler, + [ + new OptionDescriptor("verbose", nameof(Boolean), "Enable verbose output", BehaviorFlags.None) + ]); + var (_, command) = MockCommands.NewCommandDefinition("id1", "name1", "desc1", CommandTypes.Leaf, optionDescriptors); + + await _provider.ProvideHelpAsync(new TerminalHelpProviderContext(command)); + + _loggerFactory.AllLogMessages.Should().HaveCount(6); + _loggerFactory.AllLogMessages[0].Should().Be("Command:"); + _loggerFactory.AllLogMessages[1].Should().Be(" id1 (name1) 4"); + _loggerFactory.AllLogMessages[2].Should().Be(" desc1"); + _loggerFactory.AllLogMessages[3].Should().Be("Options:"); + _loggerFactory.AllLogMessages[4].Should().Be(" --verbose "); + _loggerFactory.AllLogMessages[5].Should().Be(" Enable verbose output"); + } + + [Fact] + public async Task ProvideHelpAsync_OptionWithAlias_WritesExactMessages() + { + var textHandler = new TerminalTextHandler(StringComparison.OrdinalIgnoreCase, Encoding.Unicode); + var optionDescriptors = new OptionDescriptors(textHandler, + [ + new OptionDescriptor("verbose", nameof(Boolean), "Enable verbose output", BehaviorFlags.None, "v") + ]); + var (_, command) = MockCommands.NewCommandDefinition("id1", "name1", "desc1", CommandTypes.Leaf, optionDescriptors); + + await _provider.ProvideHelpAsync(new TerminalHelpProviderContext(command)); + + _loggerFactory.AllLogMessages.Should().HaveCount(6); + _loggerFactory.AllLogMessages[0].Should().Be("Command:"); + _loggerFactory.AllLogMessages[1].Should().Be(" id1 (name1) 4"); + _loggerFactory.AllLogMessages[2].Should().Be(" desc1"); + _loggerFactory.AllLogMessages[3].Should().Be("Options:"); + _loggerFactory.AllLogMessages[4].Should().Be(" --verbose, -v "); + _loggerFactory.AllLogMessages[5].Should().Be(" Enable verbose output"); + } + + [Fact] + public async Task ProvideHelpAsync_NoOptionsNoArguments_WritesOnlyCommandMessages() + { + var (_, command) = MockCommands.NewCommandDefinition("id4", "name4", "desc4", CommandTypes.Leaf); + + await _provider.ProvideHelpAsync(new TerminalHelpProviderContext(command)); + + _loggerFactory.AllLogMessages.Should().HaveCount(3); + _loggerFactory.AllLogMessages.Should().NotContain("Options:"); + _loggerFactory.AllLogMessages.Should().NotContain("Arguments:"); + } + + [Fact] + public async Task ProvideHelpAsync_ArgumentsOnly_WritesExactMessages() + { + var textHandler = new TerminalTextHandler(StringComparison.OrdinalIgnoreCase, Encoding.Unicode); + var argumentDescriptors = new ArgumentDescriptors(textHandler, + [ + new ArgumentDescriptor(1, "file", nameof(String), "The file path", BehaviorFlags.Required), + new ArgumentDescriptor(2, "count", nameof(Int32), "The item count", BehaviorFlags.None), + ]); + var (_, command) = MockCommands.NewCommandDefinition("id1", "name1", "desc1", CommandTypes.Leaf, + argumentDescriptors: argumentDescriptors); + + await _provider.ProvideHelpAsync(new TerminalHelpProviderContext(command)); + + _loggerFactory.AllLogMessages.Should().HaveCount(8); + _loggerFactory.AllLogMessages[0].Should().Be("Command:"); + _loggerFactory.AllLogMessages[1].Should().Be(" id1 (name1) 4"); + _loggerFactory.AllLogMessages[2].Should().Be(" desc1"); + _loggerFactory.AllLogMessages[3].Should().Be("Arguments:"); + _loggerFactory.AllLogMessages[4].Should().Be(" file "); + _loggerFactory.AllLogMessages[5].Should().Be(" The file path"); + _loggerFactory.AllLogMessages[6].Should().Be(" count "); + _loggerFactory.AllLogMessages[7].Should().Be(" The item count"); + } + + [Fact] + public async Task ProvideHelpAsync_ArgumentsAndOptions_WritesExactMessages() + { + var textHandler = new TerminalTextHandler(StringComparison.OrdinalIgnoreCase, Encoding.Unicode); + var argumentDescriptors = new ArgumentDescriptors(textHandler, + [ + new ArgumentDescriptor(1, "source", nameof(String), "The source path", BehaviorFlags.Required), + ]); + var optionDescriptors = new OptionDescriptors(textHandler, + [ + new OptionDescriptor("verbose", nameof(Boolean), "Enable verbose output", BehaviorFlags.None, "v"), + ]); + var (_, command) = MockCommands.NewCommandDefinition("id1", "name1", "desc1", CommandTypes.Leaf, + args: optionDescriptors, argumentDescriptors: argumentDescriptors); + + await _provider.ProvideHelpAsync(new TerminalHelpProviderContext(command)); + + _loggerFactory.AllLogMessages.Should().HaveCount(9); + _loggerFactory.AllLogMessages[0].Should().Be("Command:"); + _loggerFactory.AllLogMessages[1].Should().Be(" id1 (name1) 4"); + _loggerFactory.AllLogMessages[2].Should().Be(" desc1"); + _loggerFactory.AllLogMessages[3].Should().Be("Arguments:"); + _loggerFactory.AllLogMessages[4].Should().Be(" source "); + _loggerFactory.AllLogMessages[5].Should().Be(" The source path"); + _loggerFactory.AllLogMessages[6].Should().Be("Options:"); + _loggerFactory.AllLogMessages[7].Should().Be(" --verbose, -v "); + _loggerFactory.AllLogMessages[8].Should().Be(" Enable verbose output"); + } + + private readonly MockListLoggerFactory _loggerFactory; + private readonly TerminalOptions _options; + private readonly TerminalLoggerHelpProvider _provider; + } +} \ No newline at end of file diff --git a/test/OneImlx.Terminal.Tests/Runtime/TerminalNoConsoleTests.cs b/test/OneImlx.Terminal.Tests/Runtime/TerminalNoConsoleTests.cs new file mode 100644 index 00000000..6d42c4b0 --- /dev/null +++ b/test/OneImlx.Terminal.Tests/Runtime/TerminalNoConsoleTests.cs @@ -0,0 +1,105 @@ +// Copyright © 2019-2026 Perpetual Intelligence L.L.C. All rights reserved. +// For license, terms, and data policies, go to: +// https://terms.perpetualintelligence.com/articles/intro.html + +using System; +using System.Threading.Tasks; +using FluentAssertions; +using Xunit; + +namespace OneImlx.Terminal.Runtime +{ + public class TerminalNoConsoleTests + { + public TerminalNoConsoleTests() + { + _console = new TerminalNoConsole(); + } + + [Fact] + public void BackgroundColor_GetSet_RoundTrips() + { + _console.BackgroundColor = ConsoleColor.Red; + _console.BackgroundColor.Should().Be(ConsoleColor.Red); + } + + [Fact] + public void ForegroundColor_GetSet_RoundTrips() + { + _console.ForegroundColor = ConsoleColor.Green; + _console.ForegroundColor.Should().Be(ConsoleColor.Green); + } + + [Fact] + public async Task ClearAsync_ReturnsCompletedTask() + { + Task result = _console.ClearAsync(); + result.IsCompleted.Should().BeTrue(); + await result; + } + + [Fact] + public void Ignore_Always_ReturnsTrue() + { + _console.Ignore("any value").Should().BeTrue(); + _console.Ignore(string.Empty).Should().BeTrue(); + _console.Ignore(null).Should().BeTrue(); + } + + [Fact] + public async Task ReadAnswerAsync_Always_ReturnsEmptyString() + { + string result = await _console.ReadAnswerAsync("question?", "yes", "no"); + result.Should().Be(string.Empty); + } + + [Fact] + public async Task ReadLineAsync_Always_ReturnsNull() + { + string? result = await _console.ReadLineAsync(); + result.Should().BeNull(); + } + + [Fact] + public async Task WriteAsync_ReturnsCompletedTask() + { + Task result = _console.WriteAsync("hello {0}", "world"); + result.IsCompleted.Should().BeTrue(); + await result; + } + + [Fact] + public async Task WriteColorAsync_ReturnsCompletedTask() + { + Task result = _console.WriteColorAsync(ConsoleColor.Cyan, "hello {0}", "world"); + result.IsCompleted.Should().BeTrue(); + await result; + } + + [Fact] + public async Task WriteLineAsync_NoArgs_ReturnsCompletedTask() + { + Task result = _console.WriteLineAsync(); + result.IsCompleted.Should().BeTrue(); + await result; + } + + [Fact] + public async Task WriteLineAsync_WithArgs_ReturnsCompletedTask() + { + Task result = _console.WriteLineAsync("hello {0}", "world"); + result.IsCompleted.Should().BeTrue(); + await result; + } + + [Fact] + public async Task WriteLineColorAsync_ReturnsCompletedTask() + { + Task result = _console.WriteLineColorAsync(ConsoleColor.Yellow, "hello {0}", "world"); + result.IsCompleted.Should().BeTrue(); + await result; + } + + private readonly TerminalNoConsole _console; + } +} \ No newline at end of file diff --git a/test/OneImlx.Terminal.Tests/Runtime/TerminalProcessorTests.cs b/test/OneImlx.Terminal.Tests/Runtime/TerminalProcessorTests.cs index c4ae25e6..e58e770b 100644 --- a/test/OneImlx.Terminal.Tests/Runtime/TerminalProcessorTests.cs +++ b/test/OneImlx.Terminal.Tests/Runtime/TerminalProcessorTests.cs @@ -229,16 +229,16 @@ public async Task AddAsync_Processes_BatchCommand_In_Order() } // Create batches for each command collection - var batch1 = TerminalInputOutput.Batch("batch1", commands1.Keys.Cast().ToArray(), commands1.Values.Cast().ToArray(), "sender1", "endpoint1"); - var batch2 = TerminalInputOutput.Batch("batch2", commands2.Keys.Cast().ToArray(), commands2.Values.Cast().ToArray(), "sender2", "endpoint2"); - var batch3 = TerminalInputOutput.Batch("batch3", commands3.Keys.Cast().ToArray(), commands3.Values.Cast().ToArray(), "sender3", "endpoint3"); - var batch4 = TerminalInputOutput.Batch("batch4", commands4.Keys.Cast().ToArray(), commands4.Values.Cast().ToArray(), "sender4", "endpoint4"); - var batch5 = TerminalInputOutput.Batch("batch5", commands5.Keys.Cast().ToArray(), commands5.Values.Cast().ToArray(), "sender5", "endpoint5"); - var batch6 = TerminalInputOutput.Batch("batch6", commands6.Keys.Cast().ToArray(), commands6.Values.Cast().ToArray(), "sender6", "endpoint6"); - var batch7 = TerminalInputOutput.Batch("batch7", commands7.Keys.Cast().ToArray(), commands7.Values.Cast().ToArray(), "sender7", "endpoint7"); - var batch8 = TerminalInputOutput.Batch("batch8", commands8.Keys.Cast().ToArray(), commands8.Values.Cast().ToArray(), "sender8", "endpoint8"); - var batch9 = TerminalInputOutput.Batch("batch9", commands9.Keys.Cast().ToArray(), commands9.Values.Cast().ToArray(), "sender9", "endpoint9"); - var batch10 = TerminalInputOutput.Batch("batch10", commands10.Keys.Cast().ToArray(), commands10.Values.Cast().ToArray(), "sender10", "endpoint10"); + var batch1 = TerminalInputOutput.Batch("batch1", [.. commands1.Keys.Cast()], [.. commands1.Values.Cast()], "sender1", "endpoint1"); + var batch2 = TerminalInputOutput.Batch("batch2", [.. commands2.Keys.Cast()], [.. commands2.Values.Cast()], "sender2", "endpoint2"); + var batch3 = TerminalInputOutput.Batch("batch3", [.. commands3.Keys.Cast()], [.. commands3.Values.Cast()], "sender3", "endpoint3"); + var batch4 = TerminalInputOutput.Batch("batch4", [.. commands4.Keys.Cast()], [.. commands4.Values.Cast()], "sender4", "endpoint4"); + var batch5 = TerminalInputOutput.Batch("batch5", [.. commands5.Keys.Cast()], [.. commands5.Values.Cast()], "sender5", "endpoint5"); + var batch6 = TerminalInputOutput.Batch("batch6", [.. commands6.Keys.Cast()], [.. commands6.Values.Cast()], "sender6", "endpoint6"); + var batch7 = TerminalInputOutput.Batch("batch7", [.. commands7.Keys.Cast()], [.. commands7.Values.Cast()], "sender7", "endpoint7"); + var batch8 = TerminalInputOutput.Batch("batch8", [.. commands8.Keys.Cast()], [.. commands8.Values.Cast()], "sender8", "endpoint8"); + var batch9 = TerminalInputOutput.Batch("batch9", [.. commands9.Keys.Cast()], [.. commands9.Values.Cast()], "sender9", "endpoint9"); + var batch10 = TerminalInputOutput.Batch("batch10", [.. commands10.Keys.Cast()], [.. commands10.Values.Cast()], "sender10", "endpoint10"); // Add all batches asynchronously Task addBatch1 = _terminalProcessor.AddAsync(batch1); @@ -296,7 +296,7 @@ public async Task AddAsync_Processes_Large_Batch() { allCommands.Add($"id{i}", $"command{i}"); } - var longBatch = TerminalInputOutput.Batch("batch_id", allCommands.Keys.ToArray(), allCommands.Values.ToArray(), "sender", "endpoint"); + var longBatch = TerminalInputOutput.Batch("batch_id", [.. allCommands.Keys], [.. allCommands.Values], "sender", "endpoint"); await _terminalProcessor.AddAsync(longBatch); await _terminalProcessor.StopProcessingAsync(5000); @@ -712,7 +712,7 @@ public async Task Stream_Does_No_Processes_Partial_Batch() var bytes3NonDelimited = JsonSerializer.SerializeToUtf8Bytes(input2); // The last batch is not delimited. - byte[] bytes = bytes1.Concat(bytes2).Concat(bytes3NonDelimited).ToArray(); + byte[] bytes = [.. bytes1, .. bytes2, .. bytes3NonDelimited]; await _terminalProcessor.StreamAsync(bytes, bytes.Length, senderId, senderEndpoint); // Allow time for processing to complete diff --git a/test/OneImlx.Terminal.Tests/Runtime/TerminalStartModeTests.cs b/test/OneImlx.Terminal.Tests/Runtime/TerminalStartModeTests.cs index cef5f771..43c1ca35 100644 --- a/test/OneImlx.Terminal.Tests/Runtime/TerminalStartModeTests.cs +++ b/test/OneImlx.Terminal.Tests/Runtime/TerminalStartModeTests.cs @@ -6,6 +6,7 @@ */ using FluentAssertions; +using OneImlx.Terminal.Shared; using Xunit; namespace OneImlx.Terminal.Runtime diff --git a/test/OneImlx.Terminal.Tests/Runtime/TerminalSystemConsoleTests.cs b/test/OneImlx.Terminal.Tests/Runtime/TerminalSystemConsoleTests.cs new file mode 100644 index 00000000..373f1b15 --- /dev/null +++ b/test/OneImlx.Terminal.Tests/Runtime/TerminalSystemConsoleTests.cs @@ -0,0 +1,177 @@ +// Copyright © 2019-2026 Perpetual Intelligence L.L.C. All rights reserved. +// For license, terms, and data policies, go to: +// https://terms.perpetualintelligence.com/articles/intro.html + +using System; +using System.IO; +using System.Threading.Tasks; +using FluentAssertions; +using Xunit; + +namespace OneImlx.Terminal.Runtime +{ + public class TerminalSystemConsoleTests : IDisposable + { + public TerminalSystemConsoleTests() + { + _console = new TerminalSystemConsole(); + _originalOut = Console.Out; + _originalIn = Console.In; + _writer = new StringWriter(); + Console.SetOut(_writer); + } + + public void Dispose() + { + Console.SetOut(_originalOut); + Console.SetIn(_originalIn); + _writer.Dispose(); + } + + [Fact] + public void Ignore_Null_ReturnsTrue() + { + _console.Ignore(null).Should().BeTrue(); + } + + [Fact] + public void Ignore_Empty_ReturnsTrue() + { + _console.Ignore(string.Empty).Should().BeTrue(); + } + + [Fact] + public void Ignore_Whitespace_ReturnsTrue() + { + _console.Ignore(" ").Should().BeTrue(); + } + + [Fact] + public void Ignore_NonWhitespace_ReturnsFalse() + { + _console.Ignore("value").Should().BeFalse(); + } + + [Fact] + public async Task WriteAsync_WritesFormattedString() + { + await _console.WriteAsync("hello {0}", "world"); + + _writer.ToString().Should().Be("hello world"); + } + + [Fact] + public async Task WriteAsync_NoFormatArgs_WritesPlainString() + { + await _console.WriteAsync("hello"); + + _writer.ToString().Should().Be("hello"); + } + + [Fact] + public async Task WriteColorAsync_WritesFormattedString() + { + await _console.WriteColorAsync(ConsoleColor.Cyan, "hello {0}", "world"); + + _writer.ToString().Should().Be("hello world"); + } + + [Fact] + public async Task WriteLineAsync_NoArgs_WritesNewLine() + { + await _console.WriteLineAsync(); + + _writer.ToString().Should().Be(Environment.NewLine); + } + + [Fact] + public async Task WriteLineAsync_PlainString_WritesLine() + { + await _console.WriteLineAsync("hello"); + + _writer.ToString().Should().Be("hello" + Environment.NewLine); + } + + [Fact] + public async Task WriteLineAsync_FormattedString_WritesLine() + { + await _console.WriteLineAsync("hello {0}", "world"); + + _writer.ToString().Should().Be("hello world" + Environment.NewLine); + } + + [Fact] + public async Task WriteLineColorAsync_WritesFormattedLine() + { + await _console.WriteLineColorAsync(ConsoleColor.Green, "hello {0}", "world"); + + _writer.ToString().Should().Be("hello world" + Environment.NewLine); + } + + [Fact] + public async Task ReadAnswerAsync_NullAnswers_PromptsWithQuestionMark() + { + Console.SetIn(new StringReader("yes")); + + string result = await _console.ReadAnswerAsync("Continue", "yes", "no"); + + _writer.ToString().Should().Be("Continue (yes/no)? "); + result.Should().Be("yes"); + } + + [Fact] + public async Task ReadAnswerAsync_WithAnswers_PromptsWithChoices() + { + Console.SetIn(new StringReader("yes")); + + string result = await _console.ReadAnswerAsync("Continue", "yes", "no"); + + _writer.ToString().Should().Be("Continue (yes/no)? "); + result.Should().Be("yes"); + } + + [Fact] + public async Task ReadLineAsync_ReturnsInputLine() + { + Console.SetIn(new StringReader("some input")); + + string? result = await _console.ReadLineAsync(); + + result.Should().Be("some input"); + } + + [Fact] + public async Task ReadLineAsync_NoInput_ReturnsNull() + { + Console.SetIn(new StringReader(string.Empty)); + + string? result = await _console.ReadLineAsync(); + + result.Should().BeNull(); + } + + [Fact] + public async Task WriteLineAsync_ConcurrentWrites_AllLinesPresent() + { + Task[] tasks = new Task[10]; + for (int i = 0; i < tasks.Length; i++) + { + int captured = i; + tasks[i] = _console.WriteLineAsync("line {0}", captured); + } + + await Task.WhenAll(tasks); + + string output = _writer.ToString(); + for (int i = 0; i < tasks.Length; i++) + { + output.Should().Contain($"line {i}"); + } + } + + private readonly TerminalSystemConsole _console; + private readonly TextWriter _originalOut; + private readonly TextReader _originalIn; + private readonly StringWriter _writer; + } +} \ No newline at end of file