mirror of
https://github.com/Tyrrrz/CliFx.git
synced 2025-10-25 15:19:17 +00:00
Compare commits
392 Commits
1.0
...
ca8368e1d9
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ca8368e1d9 | ||
|
|
d4b6a2b7a9 | ||
|
|
63f57025e8 | ||
|
|
25083eff44 | ||
|
|
7f2c00fe3a | ||
|
|
7638b997ff | ||
|
|
d80d012938 | ||
|
|
2a02d39dba | ||
|
|
c40b4f3501 | ||
|
|
3fb2a2319b | ||
|
|
1a5a0374c7 | ||
|
|
432c8a66af | ||
|
|
078ddeaf07 | ||
|
|
0fa2ebc636 | ||
|
|
c79a8c6502 | ||
|
|
cfbd8f9e76 | ||
|
|
e329f0fc78 | ||
|
|
357426c536 | ||
|
|
bc2164499b | ||
|
|
20481d4e24 | ||
|
|
2cb9335e25 | ||
|
|
f5ff6193e8 | ||
|
|
36b2b07a1d | ||
|
|
73bf19d766 | ||
|
|
093b6767c4 | ||
|
|
e4671e50bb | ||
|
|
40beb283d5 | ||
|
|
71fe231f28 | ||
|
|
8546c54c23 | ||
|
|
0fc88a42ba | ||
|
|
cb8f4b122e | ||
|
|
540f307f42 | ||
|
|
a62ce71424 | ||
|
|
ab48098e06 | ||
|
|
0532d724a1 | ||
|
|
545c7c3fbd | ||
|
|
a813436577 | ||
|
|
fcc93603a7 | ||
|
|
2d3c221b48 | ||
|
|
651146c97b | ||
|
|
82b0c6fd98 | ||
|
|
a4376c955b | ||
|
|
f7645afbdb | ||
|
|
e20672328b | ||
|
|
e99a95ef7c | ||
|
|
3e7eb08eca | ||
|
|
cfd28c133e | ||
|
|
034d3cec66 | ||
|
|
3fc7054f80 | ||
|
|
2323a57c39 | ||
|
|
bcb34055ac | ||
|
|
24fd87b1e1 | ||
|
|
fe935b5775 | ||
|
|
7dcd523bfe | ||
|
|
cad1c14474 | ||
|
|
57db910489 | ||
|
|
ae9c4e6d1e | ||
|
|
30bc1d3330 | ||
|
|
a5a4ad05a0 | ||
|
|
0b77895ca5 | ||
|
|
54994755b1 | ||
|
|
aee63cb9f2 | ||
|
|
4bdd3ccc6c | ||
|
|
6aa72e45e8 | ||
|
|
76e8d47e03 | ||
|
|
6304b8ab9c | ||
|
|
98b50d0e8e | ||
|
|
5aea869c2a | ||
|
|
425c8f4022 | ||
|
|
490398f773 | ||
|
|
5854f36756 | ||
|
|
ec6c72e6a3 | ||
|
|
41bc64be4a | ||
|
|
7df0e77e4d | ||
|
|
914e8e17cd | ||
|
|
40f106d0b0 | ||
|
|
566dd4a9a7 | ||
|
|
9beb439323 | ||
|
|
029257a915 | ||
|
|
d330fbbb63 | ||
|
|
236867f547 | ||
|
|
b41e9b4929 | ||
|
|
ff06b8896f | ||
|
|
0fe9c89fa0 | ||
|
|
8646c9de5e | ||
|
|
a33c42a163 | ||
|
|
55cea48cbd | ||
|
|
e67eda3515 | ||
|
|
4412c20e97 | ||
|
|
9eb84c6649 | ||
|
|
2ef37ab6d9 | ||
|
|
38a73772fc | ||
|
|
aed53eb090 | ||
|
|
21b601da66 | ||
|
|
a4726fcefd | ||
|
|
ab24ca8604 | ||
|
|
3533bff344 | ||
|
|
1b096b679e | ||
|
|
cb61b31e9d | ||
|
|
d8f183c404 | ||
|
|
c95b6c32d5 | ||
|
|
d2e390c691 | ||
|
|
66ef221586 | ||
|
|
2d3bb30125 | ||
|
|
5d72692aa5 | ||
|
|
3be17db784 | ||
|
|
4aef8ce8fb | ||
|
|
8c1cff3bb7 | ||
|
|
669d8bfe20 | ||
|
|
4dce7bddb4 | ||
|
|
a621e89e89 | ||
|
|
5ea11e3a23 | ||
|
|
7cb61182d2 | ||
|
|
99c59431c4 | ||
|
|
f376081489 | ||
|
|
00a1e12b5c | ||
|
|
81f8b17451 | ||
|
|
aa8315b68d | ||
|
|
e52781c25a | ||
|
|
01f29a5375 | ||
|
|
013cb8f66b | ||
|
|
9c715f458e | ||
|
|
90d93a57ee | ||
|
|
8da4a61eb7 | ||
|
|
f718370642 | ||
|
|
83c7af72bf | ||
|
|
eff84fd7ae | ||
|
|
f66fa97b87 | ||
|
|
9f309b5d4a | ||
|
|
456099591a | ||
|
|
bf7f607f9b | ||
|
|
a4041ab019 | ||
|
|
a0fde872ec | ||
|
|
f0c040c7b9 | ||
|
|
a09818d452 | ||
|
|
1c331df4b1 | ||
|
|
dc20fe9730 | ||
|
|
31ae0271b9 | ||
|
|
6ed6d2ced9 | ||
|
|
01a4846159 | ||
|
|
02dc7de127 | ||
|
|
a1ff1a1539 | ||
|
|
a02951f755 | ||
|
|
7cb25254e8 | ||
|
|
3d9ad16117 | ||
|
|
d0ad3bc45d | ||
|
|
6541ce568d | ||
|
|
32d3b66185 | ||
|
|
48f157a51e | ||
|
|
b1995fa4f7 | ||
|
|
74bc973f77 | ||
|
|
3420c3d039 | ||
|
|
b10577fec5 | ||
|
|
af96d0d31d | ||
|
|
bd29ad31cc | ||
|
|
15150cb3ed | ||
|
|
aac9c968eb | ||
|
|
85a9f157ad | ||
|
|
d24a79361d | ||
|
|
5785720f31 | ||
|
|
3f6f0b9f1b | ||
|
|
128bb5be8b | ||
|
|
36b3814f4e | ||
|
|
c4a975d5f1 | ||
|
|
79d86d39c1 | ||
|
|
c476700168 | ||
|
|
5e97ebe7f0 | ||
|
|
64cbdaaeab | ||
|
|
ae1f03914c | ||
|
|
ff25dccf8a | ||
|
|
6e0d881682 | ||
|
|
89fd42888a | ||
|
|
eeac82a6e7 | ||
|
|
c641c6fbe2 | ||
|
|
5ec732fe9a | ||
|
|
6d87411dbf | ||
|
|
ed3054c855 | ||
|
|
5d00de4dfe | ||
|
|
016ec8b186 | ||
|
|
9141092919 | ||
|
|
1fe97b0140 | ||
|
|
6ad5989c25 | ||
|
|
7e1db916fc | ||
|
|
1c69d5c80d | ||
|
|
ab87225f1f | ||
|
|
6d33c5cdad | ||
|
|
e4c899c6c2 | ||
|
|
35b3ad0d63 | ||
|
|
4e70557b47 | ||
|
|
0a8d58255a | ||
|
|
d3fbc9c643 | ||
|
|
1cbf8776be | ||
|
|
16e33f7b8f | ||
|
|
5c848056c5 | ||
|
|
864efd3179 | ||
|
|
7f206a0c77 | ||
|
|
22c15f8ec6 | ||
|
|
59373eadc2 | ||
|
|
ed3e4f471e | ||
|
|
41cb8647b5 | ||
|
|
c7015181e1 | ||
|
|
86742755e8 | ||
|
|
33f95d941d | ||
|
|
1328592cb5 | ||
|
|
0711b863ea | ||
|
|
a2f5cd54be | ||
|
|
7836ec610f | ||
|
|
2e489927eb | ||
|
|
02e8d19e48 | ||
|
|
eb7107fb0a | ||
|
|
a396009b62 | ||
|
|
1d9c7e942c | ||
|
|
0f3abb9db4 | ||
|
|
896482821c | ||
|
|
aa3094ee54 | ||
|
|
712580e3d7 | ||
|
|
c08102f85f | ||
|
|
5e684c8b36 | ||
|
|
300ae70564 | ||
|
|
76f0c77f1e | ||
|
|
0f7cea4ed1 | ||
|
|
32ee0b2bd6 | ||
|
|
4ff1e1d3e1 | ||
|
|
8e96d2701d | ||
|
|
8e307df231 | ||
|
|
ff38f4916a | ||
|
|
7cbbb220b4 | ||
|
|
ae2d4299f0 | ||
|
|
21bc69d116 | ||
|
|
05a70175cc | ||
|
|
33ec2eb3a0 | ||
|
|
f6ef6cd4c0 | ||
|
|
a9ef693dc1 | ||
|
|
98bbd666dc | ||
|
|
4e7ed830f8 | ||
|
|
ef87ff76fc | ||
|
|
2feeb21270 | ||
|
|
9990387cfa | ||
|
|
bc1bdca7c6 | ||
|
|
2a992d37df | ||
|
|
15c87aecbb | ||
|
|
10a46451ac | ||
|
|
e4c6a4174b | ||
|
|
4c65f7bbee | ||
|
|
5f21de0df5 | ||
|
|
9b01b67d98 | ||
|
|
4508f5e211 | ||
|
|
f0cbc46df4 | ||
|
|
6c96e9e173 | ||
|
|
51cca36d2a | ||
|
|
84672c92f6 | ||
|
|
b1d01898b6 | ||
|
|
441a47a1a8 | ||
|
|
8abd7219a1 | ||
|
|
df73a0bfe8 | ||
|
|
55d12dc721 | ||
|
|
a6ee44c1bb | ||
|
|
76816e22f1 | ||
|
|
daf25e59d6 | ||
|
|
f2b4e53615 | ||
|
|
2d519ab190 | ||
|
|
2d479c9cb6 | ||
|
|
2bb7e13e51 | ||
|
|
6e1dfdcdd4 | ||
|
|
5ba647e5c1 | ||
|
|
853492695f | ||
|
|
d5d72c7c50 | ||
|
|
d676b5832e | ||
|
|
28097afc1e | ||
|
|
fda96586f3 | ||
|
|
fc5af8dbbc | ||
|
|
4835e64388 | ||
|
|
0999c33f93 | ||
|
|
595805255a | ||
|
|
65eaa912cf | ||
|
|
038f48b78e | ||
|
|
d7460244b7 | ||
|
|
02766868fc | ||
|
|
8d7d25a144 | ||
|
|
17ded54e24 | ||
|
|
54a4c32ddf | ||
|
|
6d46e82145 | ||
|
|
fd4a2a18fe | ||
|
|
bfe99d620e | ||
|
|
c5a111207f | ||
|
|
544945c0e6 | ||
|
|
c616cdd750 | ||
|
|
d3c396956d | ||
|
|
d0cbbc6d9a | ||
|
|
49c7905150 | ||
|
|
f5a992a16e | ||
|
|
bade0a0048 | ||
|
|
7d3d79b861 | ||
|
|
58df63a7ad | ||
|
|
b938eef013 | ||
|
|
94f63631db | ||
|
|
90d1b11430 | ||
|
|
550e54b86d | ||
|
|
90a01e729b | ||
|
|
ac01c2aecb | ||
|
|
4acffe925c | ||
|
|
18f53eeeef | ||
|
|
03d6942540 | ||
|
|
9be811a89a | ||
|
|
f9f5a4696b | ||
|
|
d6da687170 | ||
|
|
eba66d0878 | ||
|
|
8c682766bd | ||
|
|
39d626c8d8 | ||
|
|
a338ac8ce2 | ||
|
|
11637127cb | ||
|
|
4e12aefafb | ||
|
|
144d3592fb | ||
|
|
6f82c2f0f9 | ||
|
|
b8c60717d5 | ||
|
|
fec6850c39 | ||
|
|
6a378ad946 | ||
|
|
11579f11b1 | ||
|
|
60a3b26fd1 | ||
|
|
3abdfb1acf | ||
|
|
9557d386e2 | ||
|
|
d0d024c427 | ||
|
|
f765af6061 | ||
|
|
7f2202e869 | ||
|
|
14ad9d5738 | ||
|
|
b120138de3 | ||
|
|
8df1d607c1 | ||
|
|
c06f2810b9 | ||
|
|
d52a205f13 | ||
|
|
0ec12e57c1 | ||
|
|
c322b7029c | ||
|
|
6a38c04c11 | ||
|
|
5e53107def | ||
|
|
36cea937de | ||
|
|
438d6b98ac | ||
|
|
8e1488c395 | ||
|
|
65d321b476 | ||
|
|
c6d2359d6b | ||
|
|
0d32876bad | ||
|
|
c063251d89 | ||
|
|
3831cfc7c0 | ||
|
|
b17341b56c | ||
|
|
5bda964fb5 | ||
|
|
432430489a | ||
|
|
9a20101f30 | ||
|
|
b491818779 | ||
|
|
69c24c8dfc | ||
|
|
004f906148 | ||
|
|
ac83233dc2 | ||
|
|
082910c968 | ||
|
|
11e3e0f85d | ||
|
|
42f4d7d5a7 | ||
|
|
bed22b6500 | ||
|
|
17449e0794 | ||
|
|
4732166f5f | ||
|
|
f5e37b96fc | ||
|
|
4cef596fe8 | ||
|
|
19b87717c1 | ||
|
|
7e4c6b20ff | ||
|
|
fb2071ed2b | ||
|
|
7d2f934310 | ||
|
|
95a00b0952 | ||
|
|
cb3fee65f3 | ||
|
|
65628b145a | ||
|
|
802bbfccc6 | ||
|
|
6e7742a4f3 | ||
|
|
f6a1a40471 | ||
|
|
33ca4da260 | ||
|
|
cbb72b16ae | ||
|
|
c58629e999 | ||
|
|
387fb72718 | ||
|
|
e04f0da318 | ||
|
|
d25873ee10 | ||
|
|
a28223fc8b | ||
|
|
1dab27de55 | ||
|
|
698629b153 | ||
|
|
65b66b0d27 | ||
|
|
7d3ba612c4 | ||
|
|
8c3b8d1f49 | ||
|
|
fdd39855ad | ||
|
|
671532efce | ||
|
|
5b124345b0 | ||
|
|
b812bd1423 | ||
|
|
c854f5fb8d | ||
|
|
f38bd32510 | ||
|
|
765fa5503e | ||
|
|
57f168723b | ||
|
|
79e1a2e3d7 | ||
|
|
f4f6d04857 | ||
|
|
015ede0d15 | ||
|
|
4fd7f7c3ca | ||
|
|
896dd49eb4 |
BIN
.assets/help-screen.png
Normal file
BIN
.assets/help-screen.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
63
.gitattributes
vendored
63
.gitattributes
vendored
@@ -1,63 +0,0 @@
|
||||
###############################################################################
|
||||
# Set default behavior to automatically normalize line endings.
|
||||
###############################################################################
|
||||
* text=auto
|
||||
|
||||
###############################################################################
|
||||
# Set default behavior for command prompt diff.
|
||||
#
|
||||
# This is need for earlier builds of msysgit that does not have it on by
|
||||
# default for csharp files.
|
||||
# Note: This is only used by command line
|
||||
###############################################################################
|
||||
#*.cs diff=csharp
|
||||
|
||||
###############################################################################
|
||||
# Set the merge driver for project and solution files
|
||||
#
|
||||
# Merging from the command prompt will add diff markers to the files if there
|
||||
# are conflicts (Merging from VS is not affected by the settings below, in VS
|
||||
# the diff markers are never inserted). Diff markers may cause the following
|
||||
# file extensions to fail to load in VS. An alternative would be to treat
|
||||
# these files as binary and thus will always conflict and require user
|
||||
# intervention with every merge. To do so, just uncomment the entries below
|
||||
###############################################################################
|
||||
#*.sln merge=binary
|
||||
#*.csproj merge=binary
|
||||
#*.vbproj merge=binary
|
||||
#*.vcxproj merge=binary
|
||||
#*.vcproj merge=binary
|
||||
#*.dbproj merge=binary
|
||||
#*.fsproj merge=binary
|
||||
#*.lsproj merge=binary
|
||||
#*.wixproj merge=binary
|
||||
#*.modelproj merge=binary
|
||||
#*.sqlproj merge=binary
|
||||
#*.wwaproj merge=binary
|
||||
|
||||
###############################################################################
|
||||
# behavior for image files
|
||||
#
|
||||
# image files are treated as binary by default.
|
||||
###############################################################################
|
||||
#*.jpg binary
|
||||
#*.png binary
|
||||
#*.gif binary
|
||||
|
||||
###############################################################################
|
||||
# diff behavior for common document formats
|
||||
#
|
||||
# Convert binary document formats to text before diffing them. This feature
|
||||
# is only available from the command line. Turn it on by uncommenting the
|
||||
# entries below.
|
||||
###############################################################################
|
||||
#*.doc diff=astextplain
|
||||
#*.DOC diff=astextplain
|
||||
#*.docx diff=astextplain
|
||||
#*.DOCX diff=astextplain
|
||||
#*.dot diff=astextplain
|
||||
#*.DOT diff=astextplain
|
||||
#*.pdf diff=astextplain
|
||||
#*.PDF diff=astextplain
|
||||
#*.rtf diff=astextplain
|
||||
#*.RTF diff=astextplain
|
||||
3
.github/FUNDING.yml
vendored
3
.github/FUNDING.yml
vendored
@@ -1,3 +0,0 @@
|
||||
github: Tyrrrz
|
||||
patreon: Tyrrrz
|
||||
custom: ['buymeacoffee.com/Tyrrrz', 'tyrrrz.me/donate']
|
||||
77
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
Normal file
77
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
Normal file
@@ -0,0 +1,77 @@
|
||||
name: 🐛 Bug report
|
||||
description: Report broken functionality.
|
||||
labels: [bug]
|
||||
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
- Avoid generic or vague titles such as "Something's not working" or "A couple of problems" — be as descriptive as possible.
|
||||
- Keep your issue focused on one single problem. If you have multiple bug reports, please create a separate issue for each of them.
|
||||
- Issues should represent **complete and actionable** work items. If you are unsure about something or have a question, please start a [discussion](https://github.com/Tyrrrz/CliFx/discussions/new) instead.
|
||||
- Remember that **CliFx** is an open-source project funded by the community. If you find it useful, **please consider [donating](https://tyrrrz.me/donate) to support its development**.
|
||||
|
||||
___
|
||||
|
||||
- type: input
|
||||
attributes:
|
||||
label: Version
|
||||
description: Which version of the package does this bug affect? Make sure you're not using an outdated version.
|
||||
placeholder: v1.0.0
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
attributes:
|
||||
label: Platform
|
||||
description: Which platform do you experience this bug on?
|
||||
placeholder: .NET 7.0 / Windows 11
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Steps to reproduce
|
||||
description: >
|
||||
Minimum steps required to reproduce the bug, including prerequisites, code snippets, or other relevant items.
|
||||
The information provided in this field must be readily actionable, meaning that anyone should be able to reproduce the bug by following these steps.
|
||||
If the reproduction steps are too complex to fit in this field, please provide a link to a repository instead.
|
||||
placeholder: |
|
||||
- Step 1
|
||||
- Step 2
|
||||
- Step 3
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Details
|
||||
description: Clear and thorough explanation of the bug, including any additional information you may find relevant.
|
||||
placeholder: |
|
||||
- Expected behavior: ...
|
||||
- Actual behavior: ...
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Checklist
|
||||
description: Quick list of checks to ensure that everything is in order.
|
||||
options:
|
||||
- label: I have looked through existing issues to make sure that this bug has not been reported before
|
||||
required: true
|
||||
- label: I have provided a descriptive title for this issue
|
||||
required: true
|
||||
- label: I have made sure that this bug is reproducible on the latest version of the package
|
||||
required: true
|
||||
- label: I have provided all the information needed to reproduce this bug as efficiently as possible
|
||||
required: true
|
||||
- label: I have sponsored this project
|
||||
required: false
|
||||
- label: I have not read any of the above and just checked all the boxes to submit the issue
|
||||
required: false
|
||||
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
If you are struggling to provide actionable reproduction steps, or if something else is preventing you from creating a complete bug report, please start a [discussion](https://github.com/Tyrrrz/CliFx/discussions/new) instead.
|
||||
11
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
11
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: ⚠ Feature request
|
||||
url: https://github.com/Tyrrrz/.github/blob/master/docs/project-status.md
|
||||
about: Sorry, but this project is in maintenance mode and no longer accepts new feature requests.
|
||||
- name: 🗨 Discussions
|
||||
url: https://github.com/Tyrrrz/CliFx/discussions/new
|
||||
about: Ask and answer questions.
|
||||
- name: 💬 Discord server
|
||||
url: https://discord.gg/2SUWKFnHSm
|
||||
about: Chat with the project community.
|
||||
22
.github/dependabot.yml
vendored
Normal file
22
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: github-actions
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: monthly
|
||||
labels:
|
||||
- enhancement
|
||||
groups:
|
||||
actions:
|
||||
patterns:
|
||||
- "*"
|
||||
- package-ecosystem: nuget
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: monthly
|
||||
labels:
|
||||
- enhancement
|
||||
groups:
|
||||
nuget:
|
||||
patterns:
|
||||
- "*"
|
||||
25
.github/workflows/CD.yml
vendored
25
.github/workflows/CD.yml
vendored
@@ -1,25 +0,0 @@
|
||||
name: CD
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- '*'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v1
|
||||
|
||||
- name: Install .NET Core
|
||||
uses: actions/setup-dotnet@v1
|
||||
with:
|
||||
dotnet-version: 3.1.100
|
||||
|
||||
- name: Pack
|
||||
run: dotnet pack CliFx --configuration Release
|
||||
|
||||
- name: Deploy
|
||||
run: dotnet nuget push CliFx/bin/Release/*.nupkg -s nuget.org -k ${{secrets.NUGET_TOKEN}}
|
||||
22
.github/workflows/CI.yml
vendored
22
.github/workflows/CI.yml
vendored
@@ -1,22 +0,0 @@
|
||||
name: CI
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v1
|
||||
|
||||
- name: Install .NET Core
|
||||
uses: actions/setup-dotnet@v1
|
||||
with:
|
||||
dotnet-version: 3.1.100
|
||||
|
||||
- name: Build & test
|
||||
run: dotnet test --configuration Release
|
||||
|
||||
- name: Coverage
|
||||
run: curl -s https://codecov.io/bash | bash -s -- -f CliFx.Tests/bin/Release/Coverage.xml -t ${{secrets.CODECOV_TOKEN}} -Z
|
||||
34
.github/workflows/main.yml
vendored
Normal file
34
.github/workflows/main.yml
vendored
Normal file
@@ -0,0 +1,34 @@
|
||||
name: main
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
package-version:
|
||||
type: string
|
||||
description: Package version
|
||||
required: false
|
||||
deploy:
|
||||
type: boolean
|
||||
description: Deploy package
|
||||
required: false
|
||||
default: false
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
tags:
|
||||
- "*"
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
|
||||
jobs:
|
||||
main:
|
||||
uses: Tyrrrz/.github/.github/workflows/nuget.yml@master
|
||||
with:
|
||||
deploy: ${{ inputs.deploy || github.ref_type == 'tag' }}
|
||||
package-version: ${{ inputs.package-version || (github.ref_type == 'tag' && github.ref_name) || format('0.0.0-ci-{0}', github.sha) }}
|
||||
dotnet-version: 9.0.x
|
||||
secrets:
|
||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||
NUGET_TOKEN: ${{ secrets.NUGET_TOKEN }}
|
||||
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
|
||||
341
.gitignore
vendored
341
.gitignore
vendored
@@ -1,341 +1,12 @@
|
||||
## Ignore Visual Studio temporary files, build results, and
|
||||
## files generated by popular Visual Studio add-ons.
|
||||
##
|
||||
## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
|
||||
|
||||
# User-specific files
|
||||
*.rsuser
|
||||
.vs/
|
||||
.idea/
|
||||
*.suo
|
||||
*.user
|
||||
*.userosscache
|
||||
*.sln.docstates
|
||||
|
||||
# User-specific files (MonoDevelop/Xamarin Studio)
|
||||
*.userprefs
|
||||
|
||||
# Build results
|
||||
[Dd]ebug/
|
||||
[Dd]ebugPublic/
|
||||
[Rr]elease/
|
||||
[Rr]eleases/
|
||||
x64/
|
||||
x86/
|
||||
[Aa][Rr][Mm]/
|
||||
[Aa][Rr][Mm]64/
|
||||
bld/
|
||||
[Bb]in/
|
||||
[Oo]bj/
|
||||
[Ll]og/
|
||||
bin/
|
||||
obj/
|
||||
|
||||
# Visual Studio 2015/2017 cache/options directory
|
||||
.vs/
|
||||
# Uncomment if you have tasks that create the project's static files in wwwroot
|
||||
#wwwroot/
|
||||
|
||||
# Visual Studio 2017 auto generated files
|
||||
Generated\ Files/
|
||||
|
||||
# MSTest test Results
|
||||
[Tt]est[Rr]esult*/
|
||||
[Bb]uild[Ll]og.*
|
||||
|
||||
# NUNIT
|
||||
*.VisualState.xml
|
||||
TestResult.xml
|
||||
|
||||
# Build Results of an ATL Project
|
||||
[Dd]ebugPS/
|
||||
[Rr]eleasePS/
|
||||
dlldata.c
|
||||
|
||||
# Benchmark Results
|
||||
BenchmarkDotNet.Artifacts/
|
||||
|
||||
# .NET Core
|
||||
project.lock.json
|
||||
project.fragment.lock.json
|
||||
artifacts/
|
||||
|
||||
# StyleCop
|
||||
StyleCopReport.xml
|
||||
|
||||
# Files built by Visual Studio
|
||||
*_i.c
|
||||
*_p.c
|
||||
*_h.h
|
||||
*.ilk
|
||||
*.meta
|
||||
*.obj
|
||||
*.iobj
|
||||
*.pch
|
||||
*.pdb
|
||||
*.ipdb
|
||||
*.pgc
|
||||
*.pgd
|
||||
*.rsp
|
||||
*.sbr
|
||||
*.tlb
|
||||
*.tli
|
||||
*.tlh
|
||||
*.tmp
|
||||
*.tmp_proj
|
||||
*_wpftmp.csproj
|
||||
*.log
|
||||
*.vspscc
|
||||
*.vssscc
|
||||
.builds
|
||||
*.pidb
|
||||
*.svclog
|
||||
*.scc
|
||||
|
||||
# Chutzpah Test files
|
||||
_Chutzpah*
|
||||
|
||||
# Visual C++ cache files
|
||||
ipch/
|
||||
*.aps
|
||||
*.ncb
|
||||
*.opendb
|
||||
*.opensdf
|
||||
*.sdf
|
||||
*.cachefile
|
||||
*.VC.db
|
||||
*.VC.VC.opendb
|
||||
|
||||
# Visual Studio profiler
|
||||
*.psess
|
||||
*.vsp
|
||||
*.vspx
|
||||
*.sap
|
||||
|
||||
# Visual Studio Trace Files
|
||||
*.e2e
|
||||
|
||||
# TFS 2012 Local Workspace
|
||||
$tf/
|
||||
|
||||
# Guidance Automation Toolkit
|
||||
*.gpState
|
||||
|
||||
# ReSharper is a .NET coding add-in
|
||||
_ReSharper*/
|
||||
*.[Rr]e[Ss]harper
|
||||
*.DotSettings.user
|
||||
|
||||
# JustCode is a .NET coding add-in
|
||||
.JustCode
|
||||
|
||||
# TeamCity is a build add-in
|
||||
_TeamCity*
|
||||
|
||||
# DotCover is a Code Coverage Tool
|
||||
*.dotCover
|
||||
|
||||
# AxoCover is a Code Coverage Tool
|
||||
.axoCover/*
|
||||
!.axoCover/settings.json
|
||||
|
||||
# Visual Studio code coverage results
|
||||
*.coverage
|
||||
*.coveragexml
|
||||
|
||||
# NCrunch
|
||||
_NCrunch_*
|
||||
.*crunch*.local.xml
|
||||
nCrunchTemp_*
|
||||
.ncrunchsolution
|
||||
|
||||
# MightyMoose
|
||||
*.mm.*
|
||||
AutoTest.Net/
|
||||
|
||||
# Web workbench (sass)
|
||||
.sass-cache/
|
||||
|
||||
# Installshield output folder
|
||||
[Ee]xpress/
|
||||
|
||||
# DocProject is a documentation generator add-in
|
||||
DocProject/buildhelp/
|
||||
DocProject/Help/*.HxT
|
||||
DocProject/Help/*.HxC
|
||||
DocProject/Help/*.hhc
|
||||
DocProject/Help/*.hhk
|
||||
DocProject/Help/*.hhp
|
||||
DocProject/Help/Html2
|
||||
DocProject/Help/html
|
||||
|
||||
# Click-Once directory
|
||||
publish/
|
||||
|
||||
# Publish Web Output
|
||||
*.[Pp]ublish.xml
|
||||
*.azurePubxml
|
||||
# Note: Comment the next line if you want to checkin your web deploy settings,
|
||||
# but database connection strings (with potential passwords) will be unencrypted
|
||||
*.pubxml
|
||||
*.publishproj
|
||||
|
||||
# Microsoft Azure Web App publish settings. Comment the next line if you want to
|
||||
# checkin your Azure Web App publish settings, but sensitive information contained
|
||||
# in these scripts will be unencrypted
|
||||
PublishScripts/
|
||||
|
||||
# NuGet Packages
|
||||
*.nupkg
|
||||
# The packages folder can be ignored because of Package Restore
|
||||
**/[Pp]ackages/*
|
||||
# except build/, which is used as an MSBuild target.
|
||||
!**/[Pp]ackages/build/
|
||||
# Uncomment if necessary however generally it will be regenerated when needed
|
||||
#!**/[Pp]ackages/repositories.config
|
||||
# NuGet v3's project.json files produces more ignorable files
|
||||
*.nuget.props
|
||||
*.nuget.targets
|
||||
|
||||
# Microsoft Azure Build Output
|
||||
csx/
|
||||
*.build.csdef
|
||||
|
||||
# Microsoft Azure Emulator
|
||||
ecf/
|
||||
rcf/
|
||||
|
||||
# Windows Store app package directories and files
|
||||
AppPackages/
|
||||
BundleArtifacts/
|
||||
Package.StoreAssociation.xml
|
||||
_pkginfo.txt
|
||||
*.appx
|
||||
|
||||
# Visual Studio cache files
|
||||
# files ending in .cache can be ignored
|
||||
*.[Cc]ache
|
||||
# but keep track of directories ending in .cache
|
||||
!?*.[Cc]ache/
|
||||
|
||||
# Others
|
||||
ClientBin/
|
||||
~$*
|
||||
*~
|
||||
*.dbmdl
|
||||
*.dbproj.schemaview
|
||||
*.jfm
|
||||
*.pfx
|
||||
*.publishsettings
|
||||
orleans.codegen.cs
|
||||
|
||||
# Including strong name files can present a security risk
|
||||
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
|
||||
#*.snk
|
||||
|
||||
# Since there are multiple workflows, uncomment next line to ignore bower_components
|
||||
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
|
||||
#bower_components/
|
||||
|
||||
# RIA/Silverlight projects
|
||||
Generated_Code/
|
||||
|
||||
# Backup & report files from converting an old project file
|
||||
# to a newer Visual Studio version. Backup files are not needed,
|
||||
# because we have git ;-)
|
||||
_UpgradeReport_Files/
|
||||
Backup*/
|
||||
UpgradeLog*.XML
|
||||
UpgradeLog*.htm
|
||||
ServiceFabricBackup/
|
||||
*.rptproj.bak
|
||||
|
||||
# SQL Server files
|
||||
*.mdf
|
||||
*.ldf
|
||||
*.ndf
|
||||
|
||||
# Business Intelligence projects
|
||||
*.rdl.data
|
||||
*.bim.layout
|
||||
*.bim_*.settings
|
||||
*.rptproj.rsuser
|
||||
*- Backup*.rdl
|
||||
|
||||
# Microsoft Fakes
|
||||
FakesAssemblies/
|
||||
|
||||
# GhostDoc plugin setting file
|
||||
*.GhostDoc.xml
|
||||
|
||||
# Node.js Tools for Visual Studio
|
||||
.ntvs_analysis.dat
|
||||
node_modules/
|
||||
|
||||
# Visual Studio 6 build log
|
||||
*.plg
|
||||
|
||||
# Visual Studio 6 workspace options file
|
||||
*.opt
|
||||
|
||||
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
|
||||
*.vbw
|
||||
|
||||
# Visual Studio LightSwitch build output
|
||||
**/*.HTMLClient/GeneratedArtifacts
|
||||
**/*.DesktopClient/GeneratedArtifacts
|
||||
**/*.DesktopClient/ModelManifest.xml
|
||||
**/*.Server/GeneratedArtifacts
|
||||
**/*.Server/ModelManifest.xml
|
||||
_Pvt_Extensions
|
||||
|
||||
# Paket dependency manager
|
||||
.paket/paket.exe
|
||||
paket-files/
|
||||
|
||||
# FAKE - F# Make
|
||||
.fake/
|
||||
|
||||
# JetBrains Rider
|
||||
.idea/
|
||||
*.sln.iml
|
||||
|
||||
# CodeRush personal settings
|
||||
.cr/personal
|
||||
|
||||
# Python Tools for Visual Studio (PTVS)
|
||||
__pycache__/
|
||||
*.pyc
|
||||
|
||||
# Cake - Uncomment if you are using it
|
||||
# tools/**
|
||||
# !tools/packages.config
|
||||
|
||||
# Tabs Studio
|
||||
*.tss
|
||||
|
||||
# Telerik's JustMock configuration file
|
||||
*.jmconfig
|
||||
|
||||
# BizTalk build output
|
||||
*.btp.cs
|
||||
*.btm.cs
|
||||
*.odx.cs
|
||||
*.xsd.cs
|
||||
|
||||
# OpenCover UI analysis results
|
||||
OpenCover/
|
||||
|
||||
# Azure Stream Analytics local run output
|
||||
ASALocalRun/
|
||||
|
||||
# MSBuild Binary and Structured Log
|
||||
*.binlog
|
||||
|
||||
# NVidia Nsight GPU debugger configuration file
|
||||
*.nvuser
|
||||
|
||||
# MFractors (Xamarin productivity tool) working folder
|
||||
.mfractor/
|
||||
|
||||
# Local History for Visual Studio
|
||||
.localhistory/
|
||||
|
||||
# BeatPulse healthcheck temp database
|
||||
healthchecksdb
|
||||
# Test results
|
||||
TestResults/
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 20 KiB |
@@ -1,46 +0,0 @@
|
||||
using System.Threading.Tasks;
|
||||
using BenchmarkDotNet.Attributes;
|
||||
using BenchmarkDotNet.Order;
|
||||
using CliFx.Benchmarks.Commands;
|
||||
using CommandLine;
|
||||
|
||||
namespace CliFx.Benchmarks
|
||||
{
|
||||
[SimpleJob]
|
||||
[RankColumn]
|
||||
[Orderer(SummaryOrderPolicy.FastestToSlowest)]
|
||||
public class Benchmark
|
||||
{
|
||||
private static readonly string[] Arguments = {"--str", "hello world", "-i", "13", "-b"};
|
||||
|
||||
[Benchmark(Description = "CliFx", Baseline = true)]
|
||||
public async ValueTask<int> ExecuteWithCliFx() =>
|
||||
await new CliApplicationBuilder().AddCommand(typeof(CliFxCommand)).Build().RunAsync(Arguments);
|
||||
|
||||
[Benchmark(Description = "System.CommandLine")]
|
||||
public async Task<int> ExecuteWithSystemCommandLine() =>
|
||||
await new SystemCommandLineCommand().ExecuteAsync(Arguments);
|
||||
|
||||
[Benchmark(Description = "McMaster.Extensions.CommandLineUtils")]
|
||||
public int ExecuteWithMcMaster() =>
|
||||
McMaster.Extensions.CommandLineUtils.CommandLineApplication.Execute<McMasterCommand>(Arguments);
|
||||
|
||||
[Benchmark(Description = "CommandLineParser")]
|
||||
public void ExecuteWithCommandLineParser() =>
|
||||
new Parser()
|
||||
.ParseArguments(Arguments, typeof(CommandLineParserCommand))
|
||||
.WithParsed<CommandLineParserCommand>(c => c.Execute());
|
||||
|
||||
[Benchmark(Description = "PowerArgs")]
|
||||
public void ExecuteWithPowerArgs() =>
|
||||
PowerArgs.Args.InvokeMain<PowerArgsCommand>(Arguments);
|
||||
|
||||
[Benchmark(Description = "Clipr")]
|
||||
public void ExecuteWithClipr() =>
|
||||
clipr.CliParser.Parse<CliprCommand>(Arguments).Execute();
|
||||
|
||||
[Benchmark(Description = "Cocona")]
|
||||
public void ExecuteWithCocona() =>
|
||||
Cocona.CoconaApp.Run<CoconaCommand>(Arguments);
|
||||
}
|
||||
}
|
||||
32
CliFx.Benchmarks/Benchmarks.CliFx.cs
Normal file
32
CliFx.Benchmarks/Benchmarks.CliFx.cs
Normal file
@@ -0,0 +1,32 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using BenchmarkDotNet.Attributes;
|
||||
using CliFx.Attributes;
|
||||
using CliFx.Infrastructure;
|
||||
|
||||
namespace CliFx.Benchmarks;
|
||||
|
||||
public partial class Benchmarks
|
||||
{
|
||||
[Command]
|
||||
public class CliFxCommand : ICommand
|
||||
{
|
||||
[CommandOption("str", 's')]
|
||||
public string? StrOption { get; set; }
|
||||
|
||||
[CommandOption("int", 'i')]
|
||||
public int IntOption { get; set; }
|
||||
|
||||
[CommandOption("bool", 'b')]
|
||||
public bool BoolOption { get; set; }
|
||||
|
||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||
}
|
||||
|
||||
[Benchmark(Description = "CliFx", Baseline = true)]
|
||||
public async ValueTask<int> ExecuteWithCliFx() =>
|
||||
await new CliApplicationBuilder()
|
||||
.AddCommand<CliFxCommand>()
|
||||
.Build()
|
||||
.RunAsync(Arguments, new Dictionary<string, string>());
|
||||
}
|
||||
@@ -1,6 +1,9 @@
|
||||
using clipr;
|
||||
using BenchmarkDotNet.Attributes;
|
||||
using clipr;
|
||||
|
||||
namespace CliFx.Benchmarks.Commands
|
||||
namespace CliFx.Benchmarks;
|
||||
|
||||
public partial class Benchmarks
|
||||
{
|
||||
public class CliprCommand
|
||||
{
|
||||
@@ -13,8 +16,9 @@ namespace CliFx.Benchmarks.Commands
|
||||
[NamedArgument('b', "bool", Constraint = NumArgsConstraint.Optional, Const = true)]
|
||||
public bool BoolOption { get; set; }
|
||||
|
||||
public void Execute()
|
||||
{
|
||||
}
|
||||
public void Execute() { }
|
||||
}
|
||||
}
|
||||
|
||||
[Benchmark(Description = "Clipr")]
|
||||
public void ExecuteWithClipr() => CliParser.Parse<CliprCommand>(Arguments).Execute();
|
||||
}
|
||||
19
CliFx.Benchmarks/Benchmarks.Cocona.cs
Normal file
19
CliFx.Benchmarks/Benchmarks.Cocona.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
using BenchmarkDotNet.Attributes;
|
||||
using Cocona;
|
||||
|
||||
namespace CliFx.Benchmarks;
|
||||
|
||||
public partial class Benchmarks
|
||||
{
|
||||
public class CoconaCommand
|
||||
{
|
||||
public void Execute(
|
||||
[Option("str", ['s'])] string? strOption,
|
||||
[Option("int", ['i'])] int intOption,
|
||||
[Option("bool", ['b'])] bool boolOption
|
||||
) { }
|
||||
}
|
||||
|
||||
[Benchmark(Description = "Cocona")]
|
||||
public void ExecuteWithCocona() => CoconaApp.Run<CoconaCommand>(Arguments);
|
||||
}
|
||||
27
CliFx.Benchmarks/Benchmarks.CommandLineParser.cs
Normal file
27
CliFx.Benchmarks/Benchmarks.CommandLineParser.cs
Normal file
@@ -0,0 +1,27 @@
|
||||
using BenchmarkDotNet.Attributes;
|
||||
using CommandLine;
|
||||
|
||||
namespace CliFx.Benchmarks;
|
||||
|
||||
public partial class Benchmarks
|
||||
{
|
||||
public class CommandLineParserCommand
|
||||
{
|
||||
[Option('s', "str")]
|
||||
public string? StrOption { get; set; }
|
||||
|
||||
[Option('i', "int")]
|
||||
public int IntOption { get; set; }
|
||||
|
||||
[Option('b', "bool")]
|
||||
public bool BoolOption { get; set; }
|
||||
|
||||
public void Execute() { }
|
||||
}
|
||||
|
||||
[Benchmark(Description = "CommandLineParser")]
|
||||
public void ExecuteWithCommandLineParser() =>
|
||||
new Parser()
|
||||
.ParseArguments(Arguments, typeof(CommandLineParserCommand))
|
||||
.WithParsed<CommandLineParserCommand>(c => c.Execute());
|
||||
}
|
||||
24
CliFx.Benchmarks/Benchmarks.McMaster.cs
Normal file
24
CliFx.Benchmarks/Benchmarks.McMaster.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
using BenchmarkDotNet.Attributes;
|
||||
using McMaster.Extensions.CommandLineUtils;
|
||||
|
||||
namespace CliFx.Benchmarks;
|
||||
|
||||
public partial class Benchmarks
|
||||
{
|
||||
public class McMasterCommand
|
||||
{
|
||||
[Option("--str|-s")]
|
||||
public string? StrOption { get; set; }
|
||||
|
||||
[Option("--int|-i")]
|
||||
public int IntOption { get; set; }
|
||||
|
||||
[Option("--bool|-b")]
|
||||
public bool BoolOption { get; set; }
|
||||
|
||||
public int OnExecute() => 0;
|
||||
}
|
||||
|
||||
[Benchmark(Description = "McMaster.Extensions.CommandLineUtils")]
|
||||
public int ExecuteWithMcMaster() => CommandLineApplication.Execute<McMasterCommand>(Arguments);
|
||||
}
|
||||
@@ -1,6 +1,9 @@
|
||||
using PowerArgs;
|
||||
using BenchmarkDotNet.Attributes;
|
||||
using PowerArgs;
|
||||
|
||||
namespace CliFx.Benchmarks.Commands
|
||||
namespace CliFx.Benchmarks;
|
||||
|
||||
public partial class Benchmarks
|
||||
{
|
||||
public class PowerArgsCommand
|
||||
{
|
||||
@@ -13,8 +16,9 @@ namespace CliFx.Benchmarks.Commands
|
||||
[ArgShortcut("--bool"), ArgShortcut("-b")]
|
||||
public bool BoolOption { get; set; }
|
||||
|
||||
public void Main()
|
||||
{
|
||||
}
|
||||
public void Main() { }
|
||||
}
|
||||
}
|
||||
|
||||
[Benchmark(Description = "PowerArgs")]
|
||||
public void ExecuteWithPowerArgs() => Args.InvokeMain<PowerArgsCommand>(Arguments);
|
||||
}
|
||||
33
CliFx.Benchmarks/Benchmarks.SystemCommandLine.cs
Normal file
33
CliFx.Benchmarks/Benchmarks.SystemCommandLine.cs
Normal file
@@ -0,0 +1,33 @@
|
||||
using System.CommandLine;
|
||||
using System.Threading.Tasks;
|
||||
using BenchmarkDotNet.Attributes;
|
||||
|
||||
namespace CliFx.Benchmarks;
|
||||
|
||||
public partial class Benchmarks
|
||||
{
|
||||
public class SystemCommandLineCommand
|
||||
{
|
||||
public static void ExecuteHandler(string s, int i, bool b) { }
|
||||
|
||||
public Task<int> ExecuteAsync(string[] args)
|
||||
{
|
||||
var stringOption = new Option<string>(["--str", "-s"]);
|
||||
var intOption = new Option<int>(["--int", "-i"]);
|
||||
var boolOption = new Option<bool>(["--bool", "-b"]);
|
||||
|
||||
var command = new RootCommand();
|
||||
command.AddOption(stringOption);
|
||||
command.AddOption(intOption);
|
||||
command.AddOption(boolOption);
|
||||
|
||||
command.SetHandler(ExecuteHandler, stringOption, intOption, boolOption);
|
||||
|
||||
return command.InvokeAsync(args);
|
||||
}
|
||||
}
|
||||
|
||||
[Benchmark(Description = "System.CommandLine")]
|
||||
public async Task<int> ExecuteWithSystemCommandLine() =>
|
||||
await new SystemCommandLineCommand().ExecuteAsync(Arguments);
|
||||
}
|
||||
18
CliFx.Benchmarks/Benchmarks.cs
Normal file
18
CliFx.Benchmarks/Benchmarks.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
using BenchmarkDotNet.Attributes;
|
||||
using BenchmarkDotNet.Configs;
|
||||
using BenchmarkDotNet.Order;
|
||||
using BenchmarkDotNet.Running;
|
||||
|
||||
namespace CliFx.Benchmarks;
|
||||
|
||||
[RankColumn]
|
||||
[Orderer(SummaryOrderPolicy.FastestToSlowest)]
|
||||
public partial class Benchmarks
|
||||
{
|
||||
private static readonly string[] Arguments = ["--str", "hello world", "-i", "13", "-b"];
|
||||
|
||||
public static void Main() =>
|
||||
BenchmarkRunner.Run<Benchmarks>(
|
||||
DefaultConfig.Instance.WithOptions(ConfigOptions.DisableOptimizationsValidator)
|
||||
);
|
||||
}
|
||||
@@ -1,19 +1,20 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Import Project="../CliFx.props" />
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>netcoreapp3.1</TargetFramework>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<NuGetAudit>false</NuGetAudit>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="BenchmarkDotNet" Version="0.12.0" />
|
||||
<PackageReference Include="BenchmarkDotNet" Version="0.15.2" />
|
||||
<PackageReference Include="clipr" Version="1.6.1" />
|
||||
<PackageReference Include="Cocona" Version="1.0.0" />
|
||||
<PackageReference Include="CommandLineParser" Version="2.7.82" />
|
||||
<PackageReference Include="McMaster.Extensions.CommandLineUtils" Version="2.5.0" />
|
||||
<PackageReference Include="PowerArgs" Version="3.6.0" />
|
||||
<PackageReference Include="System.CommandLine.Experimental" Version="0.3.0-alpha.19317.1" />
|
||||
<PackageReference Include="Cocona" Version="2.2.0" />
|
||||
<PackageReference Include="CommandLineParser" Version="2.9.1" />
|
||||
<PackageReference Include="CSharpier.MsBuild" Version="1.1.2" PrivateAssets="all" />
|
||||
<PackageReference Include="McMaster.Extensions.CommandLineUtils" Version="4.1.1" />
|
||||
<PackageReference Include="PowerArgs" Version="4.0.3" />
|
||||
<PackageReference Include="System.CommandLine" Version="2.0.0-beta6.25358.103" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
using System.Threading.Tasks;
|
||||
using CliFx.Attributes;
|
||||
|
||||
namespace CliFx.Benchmarks.Commands
|
||||
{
|
||||
[Command]
|
||||
public class CliFxCommand : ICommand
|
||||
{
|
||||
[CommandOption("str", 's')]
|
||||
public string? StrOption { get; set; }
|
||||
|
||||
[CommandOption("int", 'i')]
|
||||
public int IntOption { get; set; }
|
||||
|
||||
[CommandOption("bool", 'b')]
|
||||
public bool BoolOption { get; set; }
|
||||
|
||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
using Cocona;
|
||||
|
||||
namespace CliFx.Benchmarks.Commands
|
||||
{
|
||||
public class CoconaCommand
|
||||
{
|
||||
public void Execute(
|
||||
[Option("str", new []{'s'})]
|
||||
string? strOption,
|
||||
[Option("int", new []{'i'})]
|
||||
int intOption,
|
||||
[Option("bool", new []{'b'})]
|
||||
bool boolOption)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
using CommandLine;
|
||||
|
||||
namespace CliFx.Benchmarks.Commands
|
||||
{
|
||||
public class CommandLineParserCommand
|
||||
{
|
||||
[Option('s', "str")]
|
||||
public string? StrOption { get; set; }
|
||||
|
||||
[Option('i', "int")]
|
||||
public int IntOption { get; set; }
|
||||
|
||||
[Option('b', "bool")]
|
||||
public bool BoolOption { get; set; }
|
||||
|
||||
public void Execute()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
using McMaster.Extensions.CommandLineUtils;
|
||||
|
||||
namespace CliFx.Benchmarks.Commands
|
||||
{
|
||||
public class McMasterCommand
|
||||
{
|
||||
[Option("--str|-s")]
|
||||
public string? StrOption { get; set; }
|
||||
|
||||
[Option("--int|-i")]
|
||||
public int IntOption { get; set; }
|
||||
|
||||
[Option("--bool|-b")]
|
||||
public bool BoolOption { get; set; }
|
||||
|
||||
public int OnExecute() => 0;
|
||||
}
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
using System.CommandLine;
|
||||
using System.CommandLine.Invocation;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CliFx.Benchmarks.Commands
|
||||
{
|
||||
public class SystemCommandLineCommand
|
||||
{
|
||||
public static int ExecuteHandler(string s, int i, bool b) => 0;
|
||||
|
||||
public Task<int> ExecuteAsync(string[] args)
|
||||
{
|
||||
var command = new RootCommand
|
||||
{
|
||||
new Option(new[] {"--str", "-s"})
|
||||
{
|
||||
Argument = new Argument<string?>()
|
||||
},
|
||||
new Option(new[] {"--int", "-i"})
|
||||
{
|
||||
Argument = new Argument<int>()
|
||||
},
|
||||
new Option(new[] {"--bool", "-b"})
|
||||
{
|
||||
Argument = new Argument<bool>()
|
||||
}
|
||||
};
|
||||
|
||||
command.Handler = CommandHandler.Create(typeof(SystemCommandLineCommand).GetMethod(nameof(ExecuteHandler)));
|
||||
|
||||
return command.InvokeAsync(args);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
using BenchmarkDotNet.Configs;
|
||||
using BenchmarkDotNet.Running;
|
||||
|
||||
namespace CliFx.Benchmarks
|
||||
{
|
||||
public static class Program
|
||||
{
|
||||
public static void Main() =>
|
||||
BenchmarkRunner.Run(typeof(Program).Assembly, DefaultConfig.Instance
|
||||
.With(ConfigOptions.DisableOptimizationsValidator));
|
||||
}
|
||||
}
|
||||
22
CliFx.Benchmarks/Readme.md
Normal file
22
CliFx.Benchmarks/Readme.md
Normal file
@@ -0,0 +1,22 @@
|
||||
## CliFx.Benchmarks
|
||||
|
||||
All benchmarks below were ran with the following configuration:
|
||||
|
||||
```ini
|
||||
BenchmarkDotNet=v0.12.0, OS=Windows 10.0.14393.3443 (1607/AnniversaryUpdate/Redstone1)
|
||||
Intel Core i5-4460 CPU 3.20GHz (Haswell), 1 CPU, 4 logical and 4 physical cores
|
||||
Frequency=3124994 Hz, Resolution=320.0006 ns, Timer=TSC
|
||||
.NET Core SDK=3.1.100
|
||||
[Host] : .NET Core 3.1.0 (CoreCLR 4.700.19.56402, CoreFX 4.700.19.56404), X64 RyuJIT
|
||||
DefaultJob : .NET Core 3.1.0 (CoreCLR 4.700.19.56402, CoreFX 4.700.19.56404), X64 RyuJIT
|
||||
```
|
||||
|
||||
| Method | Mean | Error | StdDev | Ratio | RatioSD | Rank |
|
||||
| ------------------------------------ | ----------: | --------: | ---------: | ----: | ------: | ---: |
|
||||
| CommandLineParser | 24.79 us | 0.166 us | 0.155 us | 0.49 | 0.00 | 1 |
|
||||
| CliFx | 50.27 us | 0.248 us | 0.232 us | 1.00 | 0.00 | 2 |
|
||||
| Clipr | 160.22 us | 0.817 us | 0.764 us | 3.19 | 0.02 | 3 |
|
||||
| McMaster.Extensions.CommandLineUtils | 166.45 us | 1.111 us | 1.039 us | 3.31 | 0.03 | 4 |
|
||||
| System.CommandLine | 170.27 us | 0.599 us | 0.560 us | 3.39 | 0.02 | 5 |
|
||||
| PowerArgs | 306.12 us | 1.495 us | 1.398 us | 6.09 | 0.03 | 6 |
|
||||
| Cocona | 1,856.07 us | 48.727 us | 141.367 us | 37.88 | 2.60 | 7 |
|
||||
@@ -1,18 +1,20 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Import Project="../CliFx.props" />
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>netcoreapp3.1</TargetFramework>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<ApplicationIcon>../favicon.ico</ApplicationIcon>
|
||||
<PublishAot>true</PublishAot>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="3.1.1" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
|
||||
<PackageReference Include="CSharpier.MsBuild" Version="1.1.2" PrivateAssets="all" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.9" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\CliFx\CliFx.csproj" />
|
||||
<ProjectReference Include="..\CliFx.SourceGeneration\CliFx.SourceGeneration.csproj" ReferenceOutputAssembly="false" OutputItemType="analyzer" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -1,68 +1,55 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using CliFx.Attributes;
|
||||
using CliFx.Demo.Internal;
|
||||
using CliFx.Demo.Models;
|
||||
using CliFx.Demo.Services;
|
||||
using CliFx.Demo.Domain;
|
||||
using CliFx.Demo.Utils;
|
||||
using CliFx.Exceptions;
|
||||
using CliFx.Infrastructure;
|
||||
|
||||
namespace CliFx.Demo.Commands
|
||||
namespace CliFx.Demo.Commands;
|
||||
|
||||
[Command("book add", Description = "Adds a book to the library.")]
|
||||
public class BookAddCommand(LibraryProvider libraryProvider) : ICommand
|
||||
{
|
||||
[Command("book add", Description = "Add a book to the library.")]
|
||||
public partial class BookAddCommand : ICommand
|
||||
[CommandParameter(0, Name = "title", Description = "Book title.")]
|
||||
public required string Title { get; init; }
|
||||
|
||||
[CommandOption("author", 'a', Description = "Book author.")]
|
||||
public required string Author { get; init; }
|
||||
|
||||
[CommandOption("published", 'p', Description = "Book publish date.")]
|
||||
public DateTimeOffset Published { get; init; } =
|
||||
new(
|
||||
Random.Shared.Next(1800, 2020),
|
||||
Random.Shared.Next(1, 12),
|
||||
Random.Shared.Next(1, 28),
|
||||
Random.Shared.Next(1, 23),
|
||||
Random.Shared.Next(1, 59),
|
||||
Random.Shared.Next(1, 59),
|
||||
TimeSpan.Zero
|
||||
);
|
||||
|
||||
[CommandOption("isbn", 'n', Description = "Book ISBN.")]
|
||||
public Isbn Isbn { get; init; } =
|
||||
new(
|
||||
Random.Shared.Next(0, 999),
|
||||
Random.Shared.Next(0, 99),
|
||||
Random.Shared.Next(0, 99999),
|
||||
Random.Shared.Next(0, 99),
|
||||
Random.Shared.Next(0, 9)
|
||||
);
|
||||
|
||||
public ValueTask ExecuteAsync(IConsole console)
|
||||
{
|
||||
private readonly LibraryService _libraryService;
|
||||
if (libraryProvider.TryGetBook(Title) is not null)
|
||||
throw new CommandException($"Book '{Title}' already exists.", 10);
|
||||
|
||||
[CommandParameter(0, Name = "title", Description = "Book title.")]
|
||||
public string Title { get; set; } = "";
|
||||
var book = new Book(Title, Author, Published, Isbn);
|
||||
libraryProvider.AddBook(book);
|
||||
|
||||
[CommandOption("author", 'a', IsRequired = true, Description = "Book author.")]
|
||||
public string Author { get; set; } = "";
|
||||
console.WriteLine($"Book '{Title}' added.");
|
||||
console.WriteBook(book);
|
||||
|
||||
[CommandOption("published", 'p', Description = "Book publish date.")]
|
||||
public DateTimeOffset Published { get; set; } = CreateRandomDate();
|
||||
|
||||
[CommandOption("isbn", 'n', Description = "Book ISBN.")]
|
||||
public Isbn Isbn { get; set; } = CreateRandomIsbn();
|
||||
|
||||
public BookAddCommand(LibraryService libraryService)
|
||||
{
|
||||
_libraryService = libraryService;
|
||||
}
|
||||
|
||||
public ValueTask ExecuteAsync(IConsole console)
|
||||
{
|
||||
if (_libraryService.GetBook(Title) != null)
|
||||
throw new CommandException("Book already exists.", 1);
|
||||
|
||||
var book = new Book(Title, Author, Published, Isbn);
|
||||
_libraryService.AddBook(book);
|
||||
|
||||
console.Output.WriteLine("Book added.");
|
||||
console.RenderBook(book);
|
||||
|
||||
return default;
|
||||
}
|
||||
return default;
|
||||
}
|
||||
|
||||
public partial class BookAddCommand
|
||||
{
|
||||
private static readonly Random Random = new Random();
|
||||
|
||||
private static DateTimeOffset CreateRandomDate() => new DateTimeOffset(
|
||||
Random.Next(1800, 2020),
|
||||
Random.Next(1, 12),
|
||||
Random.Next(1, 28),
|
||||
Random.Next(1, 23),
|
||||
Random.Next(1, 59),
|
||||
Random.Next(1, 59),
|
||||
TimeSpan.Zero);
|
||||
|
||||
private static Isbn CreateRandomIsbn() => new Isbn(
|
||||
Random.Next(0, 999),
|
||||
Random.Next(0, 99),
|
||||
Random.Next(0, 99999),
|
||||
Random.Next(0, 99),
|
||||
Random.Next(0, 9));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,34 +1,27 @@
|
||||
using System.Threading.Tasks;
|
||||
using CliFx.Attributes;
|
||||
using CliFx.Demo.Internal;
|
||||
using CliFx.Demo.Services;
|
||||
using CliFx.Demo.Domain;
|
||||
using CliFx.Demo.Utils;
|
||||
using CliFx.Exceptions;
|
||||
using CliFx.Infrastructure;
|
||||
|
||||
namespace CliFx.Demo.Commands
|
||||
namespace CliFx.Demo.Commands;
|
||||
|
||||
[Command("book", Description = "Retrieves a book from the library.")]
|
||||
public class BookCommand(LibraryProvider libraryProvider) : ICommand
|
||||
{
|
||||
[Command("book", Description = "View, list, add or remove books.")]
|
||||
public class BookCommand : ICommand
|
||||
[CommandParameter(0, Name = "title", Description = "Title of the book to retrieve.")]
|
||||
public required string Title { get; init; }
|
||||
|
||||
public ValueTask ExecuteAsync(IConsole console)
|
||||
{
|
||||
private readonly LibraryService _libraryService;
|
||||
var book = libraryProvider.TryGetBook(Title);
|
||||
|
||||
[CommandParameter(0, Name = "title", Description = "Book title.")]
|
||||
public string Title { get; set; } = "";
|
||||
if (book is null)
|
||||
throw new CommandException($"Book '{Title}' not found.", 10);
|
||||
|
||||
public BookCommand(LibraryService libraryService)
|
||||
{
|
||||
_libraryService = libraryService;
|
||||
}
|
||||
console.WriteBook(book);
|
||||
|
||||
public ValueTask ExecuteAsync(IConsole console)
|
||||
{
|
||||
var book = _libraryService.GetBook(Title);
|
||||
|
||||
if (book == null)
|
||||
throw new CommandException("Book not found.", 1);
|
||||
|
||||
console.RenderBook(book);
|
||||
|
||||
return default;
|
||||
}
|
||||
return default;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,37 +1,29 @@
|
||||
using System.Threading.Tasks;
|
||||
using CliFx.Attributes;
|
||||
using CliFx.Demo.Internal;
|
||||
using CliFx.Demo.Services;
|
||||
using CliFx.Demo.Domain;
|
||||
using CliFx.Demo.Utils;
|
||||
using CliFx.Infrastructure;
|
||||
|
||||
namespace CliFx.Demo.Commands
|
||||
namespace CliFx.Demo.Commands;
|
||||
|
||||
[Command("book list", Description = "Lists all books in the library.")]
|
||||
public class BookListCommand(LibraryProvider libraryProvider) : ICommand
|
||||
{
|
||||
[Command("book list", Description = "List all books in the library.")]
|
||||
public class BookListCommand : ICommand
|
||||
public ValueTask ExecuteAsync(IConsole console)
|
||||
{
|
||||
private readonly LibraryService _libraryService;
|
||||
var library = libraryProvider.GetLibrary();
|
||||
|
||||
public BookListCommand(LibraryService libraryService)
|
||||
for (var i = 0; i < library.Books.Count; i++)
|
||||
{
|
||||
_libraryService = libraryService;
|
||||
// Add margin
|
||||
if (i != 0)
|
||||
console.WriteLine();
|
||||
|
||||
// Render book
|
||||
var book = library.Books[i];
|
||||
console.WriteBook(book);
|
||||
}
|
||||
|
||||
public ValueTask ExecuteAsync(IConsole console)
|
||||
{
|
||||
var library = _libraryService.GetLibrary();
|
||||
|
||||
var isFirst = true;
|
||||
foreach (var book in library.Books)
|
||||
{
|
||||
// Margin
|
||||
if (!isFirst)
|
||||
console.Output.WriteLine();
|
||||
isFirst = false;
|
||||
|
||||
// Render book
|
||||
console.RenderBook(book);
|
||||
}
|
||||
|
||||
return default;
|
||||
}
|
||||
return default;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,35 +1,28 @@
|
||||
using System.Threading.Tasks;
|
||||
using CliFx.Attributes;
|
||||
using CliFx.Demo.Services;
|
||||
using CliFx.Demo.Domain;
|
||||
using CliFx.Exceptions;
|
||||
using CliFx.Infrastructure;
|
||||
|
||||
namespace CliFx.Demo.Commands
|
||||
namespace CliFx.Demo.Commands;
|
||||
|
||||
[Command("book remove", Description = "Removes a book from the library.")]
|
||||
public class BookRemoveCommand(LibraryProvider libraryProvider) : ICommand
|
||||
{
|
||||
[Command("book remove", Description = "Remove a book from the library.")]
|
||||
public class BookRemoveCommand : ICommand
|
||||
[CommandParameter(0, Name = "title", Description = "Title of the book to remove.")]
|
||||
public required string Title { get; init; }
|
||||
|
||||
public ValueTask ExecuteAsync(IConsole console)
|
||||
{
|
||||
private readonly LibraryService _libraryService;
|
||||
var book = libraryProvider.TryGetBook(Title);
|
||||
|
||||
[CommandParameter(0, Name = "title", Description = "Book title.")]
|
||||
public string Title { get; set; } = "";
|
||||
if (book is null)
|
||||
throw new CommandException($"Book '{Title}' not found.", 10);
|
||||
|
||||
public BookRemoveCommand(LibraryService libraryService)
|
||||
{
|
||||
_libraryService = libraryService;
|
||||
}
|
||||
libraryProvider.RemoveBook(book);
|
||||
|
||||
public ValueTask ExecuteAsync(IConsole console)
|
||||
{
|
||||
var book = _libraryService.GetBook(Title);
|
||||
console.WriteLine($"Book '{Title}' removed.");
|
||||
|
||||
if (book == null)
|
||||
throw new CommandException("Book not found.", 1);
|
||||
|
||||
_libraryService.RemoveBook(book);
|
||||
|
||||
console.Output.WriteLine($"Book {Title} removed.");
|
||||
|
||||
return default;
|
||||
}
|
||||
return default;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
5
CliFx.Demo/Domain/Book.cs
Normal file
5
CliFx.Demo/Domain/Book.cs
Normal file
@@ -0,0 +1,5 @@
|
||||
using System;
|
||||
|
||||
namespace CliFx.Demo.Domain;
|
||||
|
||||
public record Book(string Title, string Author, DateTimeOffset Published, Isbn Isbn);
|
||||
31
CliFx.Demo/Domain/Isbn.cs
Normal file
31
CliFx.Demo/Domain/Isbn.cs
Normal file
@@ -0,0 +1,31 @@
|
||||
using System;
|
||||
|
||||
namespace CliFx.Demo.Domain;
|
||||
|
||||
public partial record Isbn(
|
||||
int EanPrefix,
|
||||
int RegistrationGroup,
|
||||
int Registrant,
|
||||
int Publication,
|
||||
int CheckDigit
|
||||
)
|
||||
{
|
||||
public override string ToString() =>
|
||||
$"{EanPrefix:000}-{RegistrationGroup:00}-{Registrant:00000}-{Publication:00}-{CheckDigit:0}";
|
||||
}
|
||||
|
||||
public partial record Isbn
|
||||
{
|
||||
public static Isbn Parse(string value, IFormatProvider formatProvider)
|
||||
{
|
||||
var components = value.Split('-', 5, StringSplitOptions.RemoveEmptyEntries);
|
||||
|
||||
return new Isbn(
|
||||
int.Parse(components[0], formatProvider),
|
||||
int.Parse(components[1], formatProvider),
|
||||
int.Parse(components[2], formatProvider),
|
||||
int.Parse(components[3], formatProvider),
|
||||
int.Parse(components[4], formatProvider)
|
||||
);
|
||||
}
|
||||
}
|
||||
27
CliFx.Demo/Domain/Library.cs
Normal file
27
CliFx.Demo/Domain/Library.cs
Normal file
@@ -0,0 +1,27 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace CliFx.Demo.Domain;
|
||||
|
||||
public partial record Library(IReadOnlyList<Book> Books)
|
||||
{
|
||||
public Library WithBook(Book book)
|
||||
{
|
||||
var books = Books.ToList();
|
||||
books.Add(book);
|
||||
|
||||
return new Library(books);
|
||||
}
|
||||
|
||||
public Library WithoutBook(Book book)
|
||||
{
|
||||
var books = Books.Where(b => b != book).ToArray();
|
||||
|
||||
return new Library(books);
|
||||
}
|
||||
}
|
||||
|
||||
public partial record Library
|
||||
{
|
||||
public static Library Empty { get; } = new([]);
|
||||
}
|
||||
6
CliFx.Demo/Domain/LibraryJsonContext.cs
Normal file
6
CliFx.Demo/Domain/LibraryJsonContext.cs
Normal file
@@ -0,0 +1,6 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace CliFx.Demo.Domain;
|
||||
|
||||
[JsonSerializable(typeof(Library))]
|
||||
public partial class LibraryJsonContext : JsonSerializerContext;
|
||||
43
CliFx.Demo/Domain/LibraryProvider.cs
Normal file
43
CliFx.Demo/Domain/LibraryProvider.cs
Normal file
@@ -0,0 +1,43 @@
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace CliFx.Demo.Domain;
|
||||
|
||||
public class LibraryProvider
|
||||
{
|
||||
private static string StorageFilePath { get; } =
|
||||
Path.Combine(Directory.GetCurrentDirectory(), "Library.json");
|
||||
|
||||
private void StoreLibrary(Library library)
|
||||
{
|
||||
var data = JsonSerializer.Serialize(library, LibraryJsonContext.Default.Library);
|
||||
File.WriteAllText(StorageFilePath, data);
|
||||
}
|
||||
|
||||
public Library GetLibrary()
|
||||
{
|
||||
if (!File.Exists(StorageFilePath))
|
||||
return Library.Empty;
|
||||
|
||||
var data = File.ReadAllText(StorageFilePath);
|
||||
|
||||
return JsonSerializer.Deserialize(data, LibraryJsonContext.Default.Library)
|
||||
?? Library.Empty;
|
||||
}
|
||||
|
||||
public Book? TryGetBook(string title) =>
|
||||
GetLibrary().Books.FirstOrDefault(b => b.Title == title);
|
||||
|
||||
public void AddBook(Book book)
|
||||
{
|
||||
var updatedLibrary = GetLibrary().WithBook(book);
|
||||
StoreLibrary(updatedLibrary);
|
||||
}
|
||||
|
||||
public void RemoveBook(Book book)
|
||||
{
|
||||
var updatedLibrary = GetLibrary().WithoutBook(book);
|
||||
StoreLibrary(updatedLibrary);
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
using System;
|
||||
using CliFx.Demo.Models;
|
||||
|
||||
namespace CliFx.Demo.Internal
|
||||
{
|
||||
internal static class Extensions
|
||||
{
|
||||
public static void RenderBook(this IConsole console, Book book)
|
||||
{
|
||||
// Title
|
||||
console.WithForegroundColor(ConsoleColor.White, () => console.Output.WriteLine(book.Title));
|
||||
|
||||
// Author
|
||||
console.Output.Write(" ");
|
||||
console.Output.Write("Author: ");
|
||||
console.WithForegroundColor(ConsoleColor.White, () => console.Output.WriteLine(book.Author));
|
||||
|
||||
// Published
|
||||
console.Output.Write(" ");
|
||||
console.Output.Write("Published: ");
|
||||
console.WithForegroundColor(ConsoleColor.White, () => console.Output.WriteLine($"{book.Published:d}"));
|
||||
|
||||
// ISBN
|
||||
console.Output.Write(" ");
|
||||
console.Output.Write("ISBN: ");
|
||||
console.WithForegroundColor(ConsoleColor.White, () => console.Output.WriteLine(book.Isbn));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
using System;
|
||||
|
||||
namespace CliFx.Demo.Models
|
||||
{
|
||||
public class Book
|
||||
{
|
||||
public string Title { get; }
|
||||
|
||||
public string Author { get; }
|
||||
|
||||
public DateTimeOffset Published { get; }
|
||||
|
||||
public Isbn Isbn { get; }
|
||||
|
||||
public Book(string title, string author, DateTimeOffset published, Isbn isbn)
|
||||
{
|
||||
Title = title;
|
||||
Author = author;
|
||||
Published = published;
|
||||
Isbn = isbn;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
using System.Linq;
|
||||
|
||||
namespace CliFx.Demo.Models
|
||||
{
|
||||
public static class Extensions
|
||||
{
|
||||
public static Library WithBook(this Library library, Book book)
|
||||
{
|
||||
var books = library.Books.ToList();
|
||||
books.Add(book);
|
||||
|
||||
return new Library(books);
|
||||
}
|
||||
|
||||
public static Library WithoutBook(this Library library, Book book)
|
||||
{
|
||||
var books = library.Books.Where(b => b != book).ToArray();
|
||||
|
||||
return new Library(books);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
using System;
|
||||
|
||||
namespace CliFx.Demo.Models
|
||||
{
|
||||
public partial class Isbn
|
||||
{
|
||||
public int EanPrefix { get; }
|
||||
|
||||
public int RegistrationGroup { get; }
|
||||
|
||||
public int Registrant { get; }
|
||||
|
||||
public int Publication { get; }
|
||||
|
||||
public int CheckDigit { get; }
|
||||
|
||||
public Isbn(int eanPrefix, int registrationGroup, int registrant, int publication, int checkDigit)
|
||||
{
|
||||
EanPrefix = eanPrefix;
|
||||
RegistrationGroup = registrationGroup;
|
||||
Registrant = registrant;
|
||||
Publication = publication;
|
||||
CheckDigit = checkDigit;
|
||||
}
|
||||
|
||||
public override string ToString() =>
|
||||
$"{EanPrefix:000}-{RegistrationGroup:00}-{Registrant:00000}-{Publication:00}-{CheckDigit:0}";
|
||||
}
|
||||
|
||||
public partial class Isbn
|
||||
{
|
||||
public static Isbn Parse(string value, IFormatProvider formatProvider)
|
||||
{
|
||||
var components = value.Split('-', 5, StringSplitOptions.RemoveEmptyEntries);
|
||||
|
||||
return new Isbn(
|
||||
int.Parse(components[0], formatProvider),
|
||||
int.Parse(components[1], formatProvider),
|
||||
int.Parse(components[2], formatProvider),
|
||||
int.Parse(components[3], formatProvider),
|
||||
int.Parse(components[4], formatProvider)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace CliFx.Demo.Models
|
||||
{
|
||||
public partial class Library
|
||||
{
|
||||
public IReadOnlyList<Book> Books { get; }
|
||||
|
||||
public Library(IReadOnlyList<Book> books)
|
||||
{
|
||||
Books = books;
|
||||
}
|
||||
}
|
||||
|
||||
public partial class Library
|
||||
{
|
||||
public static Library Empty { get; } = new Library(Array.Empty<Book>());
|
||||
}
|
||||
}
|
||||
@@ -1,35 +1,18 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using CliFx.Demo.Commands;
|
||||
using CliFx.Demo.Services;
|
||||
using CliFx;
|
||||
using CliFx.Demo.Domain;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace CliFx.Demo
|
||||
{
|
||||
public static class Program
|
||||
{
|
||||
private static IServiceProvider GetServiceProvider()
|
||||
{
|
||||
// We use Microsoft.Extensions.DependencyInjection for injecting dependencies in commands
|
||||
var services = new ServiceCollection();
|
||||
// We use Microsoft.Extensions.DependencyInjection for injecting dependencies in commands
|
||||
var services = new ServiceCollection();
|
||||
services.AddSingleton<LibraryProvider>();
|
||||
|
||||
// Register services
|
||||
services.AddSingleton<LibraryService>();
|
||||
// Register all commands as transient services
|
||||
foreach (var commandType in commandTypes)
|
||||
services.AddTransient(commandType);
|
||||
|
||||
// Register commands
|
||||
services.AddTransient<BookCommand>();
|
||||
services.AddTransient<BookAddCommand>();
|
||||
services.AddTransient<BookRemoveCommand>();
|
||||
services.AddTransient<BookListCommand>();
|
||||
|
||||
return services.BuildServiceProvider();
|
||||
}
|
||||
|
||||
public static async Task<int> Main() =>
|
||||
await new CliApplicationBuilder()
|
||||
.AddCommandsFromThisAssembly()
|
||||
.UseTypeActivator(GetServiceProvider().GetService)
|
||||
.Build()
|
||||
.RunAsync();
|
||||
}
|
||||
}
|
||||
return await new CliApplicationBuilder()
|
||||
.SetDescription("Demo application showcasing CliFx features.")
|
||||
.AddCommandsFromThisAssembly()
|
||||
.UseTypeActivator(services.BuildServiceProvider())
|
||||
.Build()
|
||||
.RunAsync();
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
# CliFx Demo Project
|
||||
|
||||
Sample command line interface for managing a library of books.
|
||||
Sample command-line interface for managing a library of books.
|
||||
|
||||
This demo project shows basic CliFx functionality such as command routing, argument parsing, autogenerated help text, and some other things.
|
||||
|
||||
You can get a list of available commands by running `CliFx.Demo --help`.
|
||||
This demo project showcases basic CliFx functionality such as command routing, argument parsing, and autogenerated help text.
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using CliFx.Demo.Models;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace CliFx.Demo.Services
|
||||
{
|
||||
public class LibraryService
|
||||
{
|
||||
private string StorageFilePath => Path.Combine(Directory.GetCurrentDirectory(), "Data.json");
|
||||
|
||||
private void StoreLibrary(Library library)
|
||||
{
|
||||
var data = JsonConvert.SerializeObject(library);
|
||||
File.WriteAllText(StorageFilePath, data);
|
||||
}
|
||||
|
||||
public Library GetLibrary()
|
||||
{
|
||||
if (!File.Exists(StorageFilePath))
|
||||
return Library.Empty;
|
||||
|
||||
var data = File.ReadAllText(StorageFilePath);
|
||||
|
||||
return JsonConvert.DeserializeObject<Library>(data);
|
||||
}
|
||||
|
||||
public Book? GetBook(string title) => GetLibrary().Books.FirstOrDefault(b => b.Title == title);
|
||||
|
||||
public void AddBook(Book book)
|
||||
{
|
||||
var updatedLibrary = GetLibrary().WithBook(book);
|
||||
StoreLibrary(updatedLibrary);
|
||||
}
|
||||
|
||||
public void RemoveBook(Book book)
|
||||
{
|
||||
var updatedLibrary = GetLibrary().WithoutBook(book);
|
||||
StoreLibrary(updatedLibrary);
|
||||
}
|
||||
}
|
||||
}
|
||||
39
CliFx.Demo/Utils/ConsoleExtensions.cs
Normal file
39
CliFx.Demo/Utils/ConsoleExtensions.cs
Normal file
@@ -0,0 +1,39 @@
|
||||
using System;
|
||||
using CliFx.Demo.Domain;
|
||||
using CliFx.Infrastructure;
|
||||
|
||||
namespace CliFx.Demo.Utils;
|
||||
|
||||
internal static class ConsoleExtensions
|
||||
{
|
||||
public static void WriteBook(this ConsoleWriter writer, Book book)
|
||||
{
|
||||
// Title
|
||||
using (writer.Console.WithForegroundColor(ConsoleColor.White))
|
||||
writer.WriteLine(book.Title);
|
||||
|
||||
// Author
|
||||
writer.Write(" ");
|
||||
writer.Write("Author: ");
|
||||
|
||||
using (writer.Console.WithForegroundColor(ConsoleColor.White))
|
||||
writer.WriteLine(book.Author);
|
||||
|
||||
// Published
|
||||
writer.Write(" ");
|
||||
writer.Write("Published: ");
|
||||
|
||||
using (writer.Console.WithForegroundColor(ConsoleColor.White))
|
||||
writer.WriteLine($"{book.Published:d}");
|
||||
|
||||
// ISBN
|
||||
writer.Write(" ");
|
||||
writer.Write("ISBN: ");
|
||||
|
||||
using (writer.Console.WithForegroundColor(ConsoleColor.White))
|
||||
writer.WriteLine(book.Isbn);
|
||||
}
|
||||
|
||||
public static void WriteBook(this IConsole console, Book book) =>
|
||||
console.Output.WriteBook(book);
|
||||
}
|
||||
27
CliFx.SourceGeneration/CliFx.SourceGeneration.csproj
Normal file
27
CliFx.SourceGeneration/CliFx.SourceGeneration.csproj
Normal file
@@ -0,0 +1,27 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netstandard2.0</TargetFramework>
|
||||
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
|
||||
<GenerateDependencyFile>true</GenerateDependencyFile>
|
||||
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
|
||||
<NoWarn>$(NoWarn);RS1035</NoWarn>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<!--
|
||||
Because this project only has a single target framework, the condition in
|
||||
Directory.Build.props does not appear to work. This is a workaround for that.
|
||||
-->
|
||||
<Nullable>annotations</Nullable>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="CSharpier.MsBuild" Version="1.1.2" PrivateAssets="all" />
|
||||
<!-- Make sure to target the lowest possible version of the compiler for wider support -->
|
||||
<PackageReference Include="Microsoft.CodeAnalysis" Version="4.11.0" PrivateAssets="all" />
|
||||
<PackageReference
|
||||
Include="Microsoft.CodeAnalysis.CSharp"
|
||||
Version="4.11.0"
|
||||
PrivateAssets="all"
|
||||
/>
|
||||
<PackageReference Include="PolyShim" Version="1.15.0" PrivateAssets="all" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
130
CliFx.SourceGeneration/CommandSchemaGenerator.cs
Normal file
130
CliFx.SourceGeneration/CommandSchemaGenerator.cs
Normal file
@@ -0,0 +1,130 @@
|
||||
using System.Linq;
|
||||
using CliFx.SourceGeneration.SemanticModel;
|
||||
using CliFx.SourceGeneration.Utils.Extensions;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
|
||||
namespace CliFx.SourceGeneration;
|
||||
|
||||
[Generator]
|
||||
public class CommandSchemaGenerator : IIncrementalGenerator
|
||||
{
|
||||
public void Initialize(IncrementalGeneratorInitializationContext context)
|
||||
{
|
||||
var values = context.SyntaxProvider.ForAttributeWithMetadataName<(
|
||||
CommandSymbol?,
|
||||
Diagnostic?
|
||||
)>(
|
||||
KnownSymbolNames.CliFxCommandAttribute,
|
||||
(n, _) => n is TypeDeclarationSyntax,
|
||||
(x, _) =>
|
||||
{
|
||||
// Predicate above ensures that these casts are safe
|
||||
var commandTypeSyntax = (TypeDeclarationSyntax)x.TargetNode;
|
||||
var commandTypeSymbol = (INamedTypeSymbol)x.TargetSymbol;
|
||||
|
||||
// Check if the target type and all its containing types are partial
|
||||
if (
|
||||
commandTypeSyntax
|
||||
.AncestorsAndSelf()
|
||||
.Any(a =>
|
||||
a is TypeDeclarationSyntax t
|
||||
&& !t.Modifiers.Any(m => m.IsKind(SyntaxKind.PartialKeyword))
|
||||
)
|
||||
)
|
||||
{
|
||||
return (
|
||||
null,
|
||||
Diagnostic.Create(
|
||||
DiagnosticDescriptors.CommandMustBePartial,
|
||||
commandTypeSyntax.Identifier.GetLocation()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Check if the target type implements ICommand
|
||||
var hasCommandInterface = commandTypeSymbol.AllInterfaces.Any(i =>
|
||||
i.DisplayNameMatches(KnownSymbolNames.CliFxCommandInterface)
|
||||
);
|
||||
|
||||
if (!hasCommandInterface)
|
||||
{
|
||||
return (
|
||||
null,
|
||||
Diagnostic.Create(
|
||||
DiagnosticDescriptors.CommandMustImplementInterface,
|
||||
commandTypeSymbol.Locations.First()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Resolve the command
|
||||
var commandAttribute = x.Attributes.First(a =>
|
||||
a.AttributeClass?.DisplayNameMatches(KnownSymbolNames.CliFxCommandAttribute)
|
||||
== true
|
||||
);
|
||||
|
||||
var command = CommandSymbol.FromSymbol(commandTypeSymbol, commandAttribute);
|
||||
|
||||
// TODO: validate command
|
||||
|
||||
return (command, null);
|
||||
}
|
||||
);
|
||||
|
||||
// Report diagnostics
|
||||
var diagnostics = values.Select((v, _) => v.Item2).WhereNotNull();
|
||||
context.RegisterSourceOutput(diagnostics, (x, d) => x.ReportDiagnostic(d));
|
||||
|
||||
// Generate command schemas
|
||||
var symbols = values.Select((v, _) => v.Item1).WhereNotNull();
|
||||
context.RegisterSourceOutput(
|
||||
symbols,
|
||||
(x, c) =>
|
||||
x.AddSource(
|
||||
$"{c.Type.FullyQualifiedName}.CommandSchema.Generated.cs",
|
||||
// lang=csharp
|
||||
$$"""
|
||||
namespace {{c.Type.Namespace}};
|
||||
|
||||
partial class {{c.Type.Name}}
|
||||
{
|
||||
public static CliFx.Schema.CommandSchema<{{c.Type.FullyQualifiedName}}> Schema { get; } = {{c.GenerateSchemaInitializationCode()}};
|
||||
}
|
||||
"""
|
||||
)
|
||||
);
|
||||
|
||||
// Generate extension methods
|
||||
var symbolsCollected = symbols.Collect();
|
||||
context.RegisterSourceOutput(
|
||||
symbolsCollected,
|
||||
(x, cs) =>
|
||||
x.AddSource(
|
||||
"CommandSchemaExtensions.Generated.cs",
|
||||
// lang=csharp
|
||||
$$"""
|
||||
namespace CliFx;
|
||||
|
||||
static partial class GeneratedExtensions
|
||||
{
|
||||
public static CliFx.CliApplicationBuilder AddCommandsFromThisAssembly(this CliFx.CliApplicationBuilder builder)
|
||||
{
|
||||
{{
|
||||
cs.Select(c => c.Type.FullyQualifiedName)
|
||||
.Select(t =>
|
||||
// lang=csharp
|
||||
$"builder.AddCommand({t}.Schema);"
|
||||
)
|
||||
.JoinToString("\n")
|
||||
}}
|
||||
|
||||
return builder;
|
||||
}
|
||||
}
|
||||
"""
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
27
CliFx.SourceGeneration/DiagnosticDescriptors.cs
Normal file
27
CliFx.SourceGeneration/DiagnosticDescriptors.cs
Normal file
@@ -0,0 +1,27 @@
|
||||
using CliFx.SourceGeneration.SemanticModel;
|
||||
using Microsoft.CodeAnalysis;
|
||||
|
||||
namespace CliFx.SourceGeneration;
|
||||
|
||||
internal static class DiagnosticDescriptors
|
||||
{
|
||||
public static DiagnosticDescriptor CommandMustBePartial { get; } =
|
||||
new(
|
||||
$"{nameof(CliFx)}_{nameof(CommandMustBePartial)}",
|
||||
"Command types must be declared as `partial`",
|
||||
"This type (and all its containing types, if present) must be declared as `partial` in order to be a valid command.",
|
||||
"CliFx",
|
||||
DiagnosticSeverity.Error,
|
||||
true
|
||||
);
|
||||
|
||||
public static DiagnosticDescriptor CommandMustImplementInterface { get; } =
|
||||
new(
|
||||
$"{nameof(CliFx)}_{nameof(CommandMustImplementInterface)}",
|
||||
$"Commands must implement the `{KnownSymbolNames.CliFxCommandInterface}` interface",
|
||||
$"This type must implement the `{KnownSymbolNames.CliFxCommandInterface}` interface in order to be a valid command.",
|
||||
"CliFx",
|
||||
DiagnosticSeverity.Error,
|
||||
true
|
||||
);
|
||||
}
|
||||
66
CliFx.SourceGeneration/SemanticModel/CommandInputSymbol.cs
Normal file
66
CliFx.SourceGeneration/SemanticModel/CommandInputSymbol.cs
Normal file
@@ -0,0 +1,66 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Microsoft.CodeAnalysis;
|
||||
|
||||
namespace CliFx.SourceGeneration.SemanticModel;
|
||||
|
||||
internal abstract partial class CommandInputSymbol(
|
||||
PropertyDescriptor property,
|
||||
bool isSequence,
|
||||
string? description,
|
||||
TypeDescriptor? converterType,
|
||||
IReadOnlyList<TypeDescriptor> validatorTypes
|
||||
)
|
||||
{
|
||||
public PropertyDescriptor Property { get; } = property;
|
||||
|
||||
public bool IsSequence { get; } = isSequence;
|
||||
|
||||
public string? Description { get; } = description;
|
||||
|
||||
public TypeDescriptor? ConverterType { get; } = converterType;
|
||||
|
||||
public IReadOnlyList<TypeDescriptor> ValidatorTypes { get; } = validatorTypes;
|
||||
}
|
||||
|
||||
internal partial class CommandInputSymbol : IEquatable<CommandInputSymbol>
|
||||
{
|
||||
public bool Equals(CommandInputSymbol? other)
|
||||
{
|
||||
if (ReferenceEquals(null, other))
|
||||
return false;
|
||||
if (ReferenceEquals(this, other))
|
||||
return true;
|
||||
|
||||
return Property.Equals(other.Property)
|
||||
&& IsSequence == other.IsSequence
|
||||
&& Description == other.Description
|
||||
&& Equals(ConverterType, other.ConverterType)
|
||||
&& ValidatorTypes.SequenceEqual(other.ValidatorTypes);
|
||||
}
|
||||
|
||||
public override bool Equals(object? obj)
|
||||
{
|
||||
if (ReferenceEquals(null, obj))
|
||||
return false;
|
||||
if (ReferenceEquals(this, obj))
|
||||
return true;
|
||||
if (obj.GetType() != GetType())
|
||||
return false;
|
||||
|
||||
return Equals((CommandInputSymbol)obj);
|
||||
}
|
||||
|
||||
public override int GetHashCode() =>
|
||||
HashCode.Combine(Property, IsSequence, Description, ConverterType, ValidatorTypes);
|
||||
}
|
||||
|
||||
internal partial class CommandInputSymbol
|
||||
{
|
||||
public static bool IsSequenceType(ITypeSymbol type) =>
|
||||
type.AllInterfaces.Any(i =>
|
||||
i.SpecialType == SpecialType.System_Collections_Generic_IEnumerable_T
|
||||
)
|
||||
&& type.SpecialType != SpecialType.System_String;
|
||||
}
|
||||
90
CliFx.SourceGeneration/SemanticModel/CommandOptionSymbol.cs
Normal file
90
CliFx.SourceGeneration/SemanticModel/CommandOptionSymbol.cs
Normal file
@@ -0,0 +1,90 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using CliFx.SourceGeneration.Utils.Extensions;
|
||||
using Microsoft.CodeAnalysis;
|
||||
|
||||
namespace CliFx.SourceGeneration.SemanticModel;
|
||||
|
||||
internal partial class CommandOptionSymbol(
|
||||
PropertyDescriptor property,
|
||||
bool isSequence,
|
||||
string? name,
|
||||
char? shortName,
|
||||
string? environmentVariable,
|
||||
bool isRequired,
|
||||
string? description,
|
||||
TypeDescriptor? converterType,
|
||||
IReadOnlyList<TypeDescriptor> validatorTypes
|
||||
) : CommandInputSymbol(property, isSequence, description, converterType, validatorTypes)
|
||||
{
|
||||
public string? Name { get; } = name;
|
||||
|
||||
public char? ShortName { get; } = shortName;
|
||||
|
||||
public string? EnvironmentVariable { get; } = environmentVariable;
|
||||
|
||||
public bool IsRequired { get; } = isRequired;
|
||||
}
|
||||
|
||||
internal partial class CommandOptionSymbol : IEquatable<CommandOptionSymbol>
|
||||
{
|
||||
public bool Equals(CommandOptionSymbol? other)
|
||||
{
|
||||
if (ReferenceEquals(null, other))
|
||||
return false;
|
||||
if (ReferenceEquals(this, other))
|
||||
return true;
|
||||
|
||||
return base.Equals(other)
|
||||
&& Name == other.Name
|
||||
&& ShortName == other.ShortName
|
||||
&& EnvironmentVariable == other.EnvironmentVariable
|
||||
&& IsRequired == other.IsRequired;
|
||||
}
|
||||
|
||||
public override bool Equals(object? obj)
|
||||
{
|
||||
if (ReferenceEquals(null, obj))
|
||||
return false;
|
||||
if (ReferenceEquals(this, obj))
|
||||
return true;
|
||||
if (obj.GetType() != GetType())
|
||||
return false;
|
||||
|
||||
return Equals((CommandOptionSymbol)obj);
|
||||
}
|
||||
|
||||
public override int GetHashCode() =>
|
||||
HashCode.Combine(base.GetHashCode(), Name, ShortName, EnvironmentVariable, IsRequired);
|
||||
}
|
||||
|
||||
internal partial class CommandOptionSymbol
|
||||
{
|
||||
public static CommandOptionSymbol FromSymbol(
|
||||
IPropertySymbol property,
|
||||
AttributeData attribute
|
||||
) =>
|
||||
new(
|
||||
PropertyDescriptor.FromSymbol(property),
|
||||
IsSequenceType(property.Type),
|
||||
attribute
|
||||
.ConstructorArguments.FirstOrDefault(a =>
|
||||
a.Type?.SpecialType == SpecialType.System_String
|
||||
)
|
||||
.Value as string,
|
||||
attribute
|
||||
.ConstructorArguments.FirstOrDefault(a =>
|
||||
a.Type?.SpecialType == SpecialType.System_Char
|
||||
)
|
||||
.Value as char?,
|
||||
attribute.GetNamedArgumentValue("EnvironmentVariable", default(string)),
|
||||
attribute.GetNamedArgumentValue("IsRequired", property.IsRequired),
|
||||
attribute.GetNamedArgumentValue("Description", default(string)),
|
||||
TypeDescriptor.FromSymbol(attribute.GetNamedArgumentValue<ITypeSymbol?>("Converter")),
|
||||
attribute
|
||||
.GetNamedArgumentValues<ITypeSymbol>("Validators")
|
||||
.Select(TypeDescriptor.FromSymbol)
|
||||
.ToArray()
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using CliFx.SourceGeneration.Utils.Extensions;
|
||||
using Microsoft.CodeAnalysis;
|
||||
|
||||
namespace CliFx.SourceGeneration.SemanticModel;
|
||||
|
||||
internal partial class CommandParameterSymbol(
|
||||
PropertyDescriptor property,
|
||||
bool isSequence,
|
||||
int order,
|
||||
string name,
|
||||
bool isRequired,
|
||||
string? description,
|
||||
TypeDescriptor? converterType,
|
||||
IReadOnlyList<TypeDescriptor> validatorTypes
|
||||
) : CommandInputSymbol(property, isSequence, description, converterType, validatorTypes)
|
||||
{
|
||||
public int Order { get; } = order;
|
||||
|
||||
public string Name { get; } = name;
|
||||
|
||||
public bool IsRequired { get; } = isRequired;
|
||||
}
|
||||
|
||||
internal partial class CommandParameterSymbol : IEquatable<CommandParameterSymbol>
|
||||
{
|
||||
public bool Equals(CommandParameterSymbol? other)
|
||||
{
|
||||
if (ReferenceEquals(null, other))
|
||||
return false;
|
||||
if (ReferenceEquals(this, other))
|
||||
return true;
|
||||
|
||||
return base.Equals(other)
|
||||
&& Order == other.Order
|
||||
&& Name == other.Name
|
||||
&& IsRequired == other.IsRequired;
|
||||
}
|
||||
|
||||
public override bool Equals(object? obj)
|
||||
{
|
||||
if (ReferenceEquals(null, obj))
|
||||
return false;
|
||||
if (ReferenceEquals(this, obj))
|
||||
return true;
|
||||
if (obj.GetType() != GetType())
|
||||
return false;
|
||||
|
||||
return Equals((CommandParameterSymbol)obj);
|
||||
}
|
||||
|
||||
public override int GetHashCode() =>
|
||||
HashCode.Combine(base.GetHashCode(), Order, Name, IsRequired);
|
||||
}
|
||||
|
||||
internal partial class CommandParameterSymbol
|
||||
{
|
||||
public static CommandParameterSymbol FromSymbol(
|
||||
IPropertySymbol property,
|
||||
AttributeData attribute
|
||||
) =>
|
||||
new(
|
||||
PropertyDescriptor.FromSymbol(property),
|
||||
IsSequenceType(property.Type),
|
||||
(int)attribute.ConstructorArguments.First().Value!,
|
||||
attribute.GetNamedArgumentValue("Name", default(string)),
|
||||
attribute.GetNamedArgumentValue("IsRequired", true),
|
||||
attribute.GetNamedArgumentValue("Description", default(string)),
|
||||
TypeDescriptor.FromSymbol(attribute.GetNamedArgumentValue<ITypeSymbol>("Converter")),
|
||||
attribute
|
||||
.GetNamedArgumentValues<ITypeSymbol>("Validators")
|
||||
.Select(TypeDescriptor.FromSymbol)
|
||||
.ToArray()
|
||||
);
|
||||
}
|
||||
165
CliFx.SourceGeneration/SemanticModel/CommandSymbol.cs
Normal file
165
CliFx.SourceGeneration/SemanticModel/CommandSymbol.cs
Normal file
@@ -0,0 +1,165 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using CliFx.SourceGeneration.Utils.Extensions;
|
||||
using Microsoft.CodeAnalysis;
|
||||
|
||||
namespace CliFx.SourceGeneration.SemanticModel;
|
||||
|
||||
internal partial class CommandSymbol(
|
||||
TypeDescriptor type,
|
||||
string? name,
|
||||
string? description,
|
||||
IReadOnlyList<CommandInputSymbol> inputs
|
||||
)
|
||||
{
|
||||
public TypeDescriptor Type { get; } = type;
|
||||
|
||||
public string? Name { get; } = name;
|
||||
|
||||
public string? Description { get; } = description;
|
||||
|
||||
public IReadOnlyList<CommandInputSymbol> Inputs { get; } = inputs;
|
||||
|
||||
public IReadOnlyList<CommandParameterSymbol> Parameters =>
|
||||
Inputs.OfType<CommandParameterSymbol>().ToArray();
|
||||
|
||||
public IReadOnlyList<CommandOptionSymbol> Options =>
|
||||
Inputs.OfType<CommandOptionSymbol>().ToArray();
|
||||
|
||||
private string GeneratePropertyBindingInitializationCode(PropertyDescriptor property) =>
|
||||
// lang=csharp
|
||||
$$"""
|
||||
new CliFx.Schema.PropertyBinding<{{Type.FullyQualifiedName}}, {{property
|
||||
.Type
|
||||
.FullyQualifiedName}}>(
|
||||
(obj) => obj.{{property.Name}},
|
||||
(obj, value) => obj.{{property.Name}} = value
|
||||
)
|
||||
""";
|
||||
|
||||
private string GenerateSchemaInitializationCode(CommandInputSymbol input) =>
|
||||
input switch
|
||||
{
|
||||
CommandParameterSymbol parameter =>
|
||||
// lang=csharp
|
||||
$$"""
|
||||
new CliFx.Schema.CommandParameterSchema<{{Type.FullyQualifiedName}}, {{parameter
|
||||
.Property
|
||||
.Type
|
||||
.FullyQualifiedName}}>(
|
||||
{{GeneratePropertyBindingInitializationCode(parameter.Property)}},
|
||||
{{parameter.IsSequence}},
|
||||
{{parameter.Order}},
|
||||
"{{parameter.Name}}",
|
||||
{{parameter.IsRequired}},
|
||||
"{{parameter.Description}}",
|
||||
// TODO,
|
||||
// TODO
|
||||
);
|
||||
""",
|
||||
CommandOptionSymbol option =>
|
||||
// lang=csharp
|
||||
$$"""
|
||||
new CliFx.Schema.CommandOptionSchema<{{Type.FullyQualifiedName}}, {{option
|
||||
.Property
|
||||
.Type
|
||||
.FullyQualifiedName}}>(
|
||||
{{GeneratePropertyBindingInitializationCode(option.Property)}},
|
||||
{{option.IsSequence}},
|
||||
"{{option.Name}}",
|
||||
'{{option.ShortName}}',
|
||||
"{{option.EnvironmentVariable}}",
|
||||
{{option.IsRequired}},
|
||||
"{{option.Description}}",
|
||||
// TODO,
|
||||
// TODO
|
||||
);
|
||||
""",
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(input), input, null),
|
||||
};
|
||||
|
||||
public string GenerateSchemaInitializationCode() =>
|
||||
// lang=csharp
|
||||
$$"""
|
||||
new CliFx.Schema.CommandSchema<{{Type.FullyQualifiedName}}>(
|
||||
"{{Name}}",
|
||||
"{{Description}}",
|
||||
new CliFx.Schema.CommandInputSchema[]
|
||||
{
|
||||
{{Inputs.Select(GenerateSchemaInitializationCode).JoinToString(",\n")}}
|
||||
}
|
||||
)
|
||||
""";
|
||||
}
|
||||
|
||||
internal partial class CommandSymbol : IEquatable<CommandSymbol>
|
||||
{
|
||||
public bool Equals(CommandSymbol? other)
|
||||
{
|
||||
if (ReferenceEquals(null, other))
|
||||
return false;
|
||||
if (ReferenceEquals(this, other))
|
||||
return true;
|
||||
|
||||
return Type.Equals(other.Type)
|
||||
&& Name == other.Name
|
||||
&& Description == other.Description
|
||||
&& Inputs.SequenceEqual(other.Inputs);
|
||||
}
|
||||
|
||||
public override bool Equals(object? obj)
|
||||
{
|
||||
if (ReferenceEquals(null, obj))
|
||||
return false;
|
||||
if (ReferenceEquals(this, obj))
|
||||
return true;
|
||||
if (obj.GetType() != GetType())
|
||||
return false;
|
||||
|
||||
return Equals((CommandSymbol)obj);
|
||||
}
|
||||
|
||||
public override int GetHashCode() => HashCode.Combine(Type, Name, Description, Inputs);
|
||||
}
|
||||
|
||||
internal partial class CommandSymbol
|
||||
{
|
||||
public static CommandSymbol FromSymbol(INamedTypeSymbol symbol, AttributeData attribute)
|
||||
{
|
||||
var inputs = new List<CommandInputSymbol>();
|
||||
foreach (var property in symbol.GetMembers().OfType<IPropertySymbol>())
|
||||
{
|
||||
var parameterAttribute = property
|
||||
.GetAttributes()
|
||||
.FirstOrDefault(a =>
|
||||
a.AttributeClass?.Name == KnownSymbolNames.CliFxCommandParameterAttribute
|
||||
);
|
||||
|
||||
if (parameterAttribute is not null)
|
||||
{
|
||||
inputs.Add(CommandParameterSymbol.FromSymbol(property, parameterAttribute));
|
||||
continue;
|
||||
}
|
||||
|
||||
var optionAttribute = property
|
||||
.GetAttributes()
|
||||
.FirstOrDefault(a =>
|
||||
a.AttributeClass?.Name == KnownSymbolNames.CliFxCommandOptionAttribute
|
||||
);
|
||||
|
||||
if (optionAttribute is not null)
|
||||
{
|
||||
inputs.Add(CommandOptionSymbol.FromSymbol(property, optionAttribute));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return new CommandSymbol(
|
||||
TypeDescriptor.FromSymbol(symbol),
|
||||
attribute.ConstructorArguments.FirstOrDefault().Value as string,
|
||||
attribute.GetNamedArgumentValue("Description", default(string)),
|
||||
inputs
|
||||
);
|
||||
}
|
||||
}
|
||||
10
CliFx.SourceGeneration/SemanticModel/KnownSymbolNames.cs
Normal file
10
CliFx.SourceGeneration/SemanticModel/KnownSymbolNames.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
namespace CliFx.SourceGeneration.SemanticModel;
|
||||
|
||||
internal static class KnownSymbolNames
|
||||
{
|
||||
public const string CliFxCommandInterface = "CliFx.ICommand";
|
||||
public const string CliFxCommandAttribute = "CliFx.Attributes.CommandAttribute";
|
||||
public const string CliFxCommandParameterAttribute =
|
||||
"CliFx.Attributes.CommandParameterAttribute";
|
||||
public const string CliFxCommandOptionAttribute = "CliFx.Attributes.CommandOptionAttribute";
|
||||
}
|
||||
44
CliFx.SourceGeneration/SemanticModel/PropertyDescriptor.cs
Normal file
44
CliFx.SourceGeneration/SemanticModel/PropertyDescriptor.cs
Normal file
@@ -0,0 +1,44 @@
|
||||
using System;
|
||||
using Microsoft.CodeAnalysis;
|
||||
|
||||
namespace CliFx.SourceGeneration.SemanticModel;
|
||||
|
||||
internal partial class PropertyDescriptor(TypeDescriptor type, string name)
|
||||
{
|
||||
public TypeDescriptor Type { get; } = type;
|
||||
|
||||
public string Name { get; } = name;
|
||||
}
|
||||
|
||||
internal partial class PropertyDescriptor : IEquatable<PropertyDescriptor>
|
||||
{
|
||||
public bool Equals(PropertyDescriptor? other)
|
||||
{
|
||||
if (ReferenceEquals(null, other))
|
||||
return false;
|
||||
if (ReferenceEquals(this, other))
|
||||
return true;
|
||||
|
||||
return Type.Equals(other.Type) && Name == other.Name;
|
||||
}
|
||||
|
||||
public override bool Equals(object? obj)
|
||||
{
|
||||
if (ReferenceEquals(null, obj))
|
||||
return false;
|
||||
if (ReferenceEquals(this, obj))
|
||||
return true;
|
||||
if (obj.GetType() != GetType())
|
||||
return false;
|
||||
|
||||
return Equals((PropertyDescriptor)obj);
|
||||
}
|
||||
|
||||
public override int GetHashCode() => HashCode.Combine(Type, Name);
|
||||
}
|
||||
|
||||
internal partial class PropertyDescriptor
|
||||
{
|
||||
public static PropertyDescriptor FromSymbol(IPropertySymbol symbol) =>
|
||||
new(TypeDescriptor.FromSymbol(symbol.Type), symbol.Name);
|
||||
}
|
||||
47
CliFx.SourceGeneration/SemanticModel/TypeDescriptor.cs
Normal file
47
CliFx.SourceGeneration/SemanticModel/TypeDescriptor.cs
Normal file
@@ -0,0 +1,47 @@
|
||||
using System;
|
||||
using CliFx.SourceGeneration.Utils.Extensions;
|
||||
using Microsoft.CodeAnalysis;
|
||||
|
||||
namespace CliFx.SourceGeneration.SemanticModel;
|
||||
|
||||
internal partial class TypeDescriptor(string fullyQualifiedName)
|
||||
{
|
||||
public string FullyQualifiedName { get; } = fullyQualifiedName;
|
||||
|
||||
public string Namespace { get; } = fullyQualifiedName.SubstringUntilLast(".");
|
||||
|
||||
public string Name { get; } = fullyQualifiedName.SubstringAfterLast(".");
|
||||
}
|
||||
|
||||
internal partial class TypeDescriptor : IEquatable<TypeDescriptor>
|
||||
{
|
||||
public bool Equals(TypeDescriptor? other)
|
||||
{
|
||||
if (ReferenceEquals(null, other))
|
||||
return false;
|
||||
if (ReferenceEquals(this, other))
|
||||
return true;
|
||||
|
||||
return FullyQualifiedName == other.FullyQualifiedName;
|
||||
}
|
||||
|
||||
public override bool Equals(object? obj)
|
||||
{
|
||||
if (ReferenceEquals(null, obj))
|
||||
return false;
|
||||
if (ReferenceEquals(this, obj))
|
||||
return true;
|
||||
if (obj.GetType() != GetType())
|
||||
return false;
|
||||
|
||||
return Equals((TypeDescriptor)obj);
|
||||
}
|
||||
|
||||
public override int GetHashCode() => FullyQualifiedName.GetHashCode();
|
||||
}
|
||||
|
||||
internal partial class TypeDescriptor
|
||||
{
|
||||
public static TypeDescriptor FromSymbol(ITypeSymbol symbol) =>
|
||||
new(symbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat));
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace CliFx.SourceGeneration.Utils.Extensions;
|
||||
|
||||
internal static class CollectionExtensions
|
||||
{
|
||||
public static IEnumerable<T> WhereNotNull<T>(this IEnumerable<T?> source)
|
||||
where T : class
|
||||
{
|
||||
foreach (var i in source)
|
||||
{
|
||||
if (i is not null)
|
||||
yield return i;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
using System;
|
||||
|
||||
namespace CliFx.SourceGeneration.Utils.Extensions;
|
||||
|
||||
internal static class GenericExtensions
|
||||
{
|
||||
public static TOut Pipe<TIn, TOut>(this TIn input, Func<TIn, TOut> transform) =>
|
||||
transform(input);
|
||||
}
|
||||
39
CliFx.SourceGeneration/Utils/Extensions/RoslynExtensions.cs
Normal file
39
CliFx.SourceGeneration/Utils/Extensions/RoslynExtensions.cs
Normal file
@@ -0,0 +1,39 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using Microsoft.CodeAnalysis;
|
||||
|
||||
namespace CliFx.SourceGeneration.Utils.Extensions;
|
||||
|
||||
internal static class RoslynExtensions
|
||||
{
|
||||
public static bool DisplayNameMatches(this ISymbol symbol, string name) =>
|
||||
string.Equals(
|
||||
// Fully qualified name, without `global::`
|
||||
symbol.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat),
|
||||
name,
|
||||
StringComparison.Ordinal
|
||||
);
|
||||
|
||||
public static T GetNamedArgumentValue<T>(
|
||||
this AttributeData attribute,
|
||||
string name,
|
||||
T defaultValue = default
|
||||
) =>
|
||||
attribute.NamedArguments.FirstOrDefault(i => i.Key == name).Value.Value is T valueAsT
|
||||
? valueAsT
|
||||
: defaultValue;
|
||||
|
||||
public static IReadOnlyList<T> GetNamedArgumentValues<T>(
|
||||
this AttributeData attribute,
|
||||
string name
|
||||
)
|
||||
where T : class =>
|
||||
attribute.NamedArguments.FirstOrDefault(i => i.Key == name).Value.Values.CastArray<T>();
|
||||
|
||||
public static IncrementalValuesProvider<T> WhereNotNull<T>(
|
||||
this IncrementalValuesProvider<T?> values
|
||||
)
|
||||
where T : class => values.Where(i => i is not null);
|
||||
}
|
||||
30
CliFx.SourceGeneration/Utils/Extensions/StringExtensions.cs
Normal file
30
CliFx.SourceGeneration/Utils/Extensions/StringExtensions.cs
Normal file
@@ -0,0 +1,30 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace CliFx.SourceGeneration.Utils.Extensions;
|
||||
|
||||
internal static class StringExtensions
|
||||
{
|
||||
public static string SubstringUntilLast(
|
||||
this string str,
|
||||
string sub,
|
||||
StringComparison comparison = StringComparison.Ordinal
|
||||
)
|
||||
{
|
||||
var index = str.LastIndexOf(sub, comparison);
|
||||
return index < 0 ? str : str[..index];
|
||||
}
|
||||
|
||||
public static string SubstringAfterLast(
|
||||
this string str,
|
||||
string sub,
|
||||
StringComparison comparison = StringComparison.Ordinal
|
||||
)
|
||||
{
|
||||
var index = str.LastIndexOf(sub, comparison);
|
||||
return index >= 0 ? str.Substring(index + sub.Length, str.Length - index - sub.Length) : "";
|
||||
}
|
||||
|
||||
public static string JoinToString<T>(this IEnumerable<T> source, string separator) =>
|
||||
string.Join(separator, source);
|
||||
}
|
||||
@@ -1,13 +1,18 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Import Project="../CliFx.props" />
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>netcoreapp3.1</TargetFramework>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<ApplicationIcon>../favicon.ico</ApplicationIcon>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="CSharpier.MsBuild" Version="1.1.2" PrivateAssets="all" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\CliFx\CliFx.csproj" />
|
||||
<ProjectReference Include="..\CliFx.SourceGeneration\CliFx.SourceGeneration.csproj" ReferenceOutputAssembly="false" OutputItemType="analyzer" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
27
CliFx.Tests.Dummy/Commands/CancellationTestCommand.cs
Normal file
27
CliFx.Tests.Dummy/Commands/CancellationTestCommand.cs
Normal file
@@ -0,0 +1,27 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using CliFx.Attributes;
|
||||
using CliFx.Infrastructure;
|
||||
|
||||
namespace CliFx.Tests.Dummy.Commands;
|
||||
|
||||
[Command("cancel-test")]
|
||||
public class CancellationTestCommand : ICommand
|
||||
{
|
||||
public async ValueTask ExecuteAsync(IConsole console)
|
||||
{
|
||||
try
|
||||
{
|
||||
console.WriteLine("Started.");
|
||||
|
||||
await Task.Delay(TimeSpan.FromSeconds(3), console.RegisterCancellationHandler());
|
||||
|
||||
console.WriteLine("Completed.");
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
console.WriteLine("Cancelled.");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
23
CliFx.Tests.Dummy/Commands/ConsoleTestCommand.cs
Normal file
23
CliFx.Tests.Dummy/Commands/ConsoleTestCommand.cs
Normal file
@@ -0,0 +1,23 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using CliFx.Attributes;
|
||||
using CliFx.Infrastructure;
|
||||
|
||||
namespace CliFx.Tests.Dummy.Commands;
|
||||
|
||||
[Command("console-test")]
|
||||
public class ConsoleTestCommand : ICommand
|
||||
{
|
||||
public ValueTask ExecuteAsync(IConsole console)
|
||||
{
|
||||
var input = console.Input.ReadToEnd();
|
||||
|
||||
using (console.WithColors(ConsoleColor.Black, ConsoleColor.White))
|
||||
{
|
||||
console.Output.WriteLine(input);
|
||||
console.Error.WriteLine(input);
|
||||
}
|
||||
|
||||
return default;
|
||||
}
|
||||
}
|
||||
18
CliFx.Tests.Dummy/Commands/EnvironmentTestCommand.cs
Normal file
18
CliFx.Tests.Dummy/Commands/EnvironmentTestCommand.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
using System.Threading.Tasks;
|
||||
using CliFx.Attributes;
|
||||
using CliFx.Infrastructure;
|
||||
|
||||
namespace CliFx.Tests.Dummy.Commands;
|
||||
|
||||
[Command("env-test")]
|
||||
public class EnvironmentTestCommand : ICommand
|
||||
{
|
||||
[CommandOption("target", EnvironmentVariable = "ENV_TARGET")]
|
||||
public string GreetingTarget { get; init; } = "World";
|
||||
|
||||
public ValueTask ExecuteAsync(IConsole console)
|
||||
{
|
||||
console.WriteLine($"Hello {GreetingTarget}!");
|
||||
return default;
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
using System.Threading.Tasks;
|
||||
using CliFx.Attributes;
|
||||
|
||||
namespace CliFx.Tests.Dummy.Commands
|
||||
{
|
||||
[Command]
|
||||
public class HelloWorldCommand : ICommand
|
||||
{
|
||||
[CommandOption("target", EnvironmentVariableName = "ENV_TARGET")]
|
||||
public string Target { get; set; } = "World";
|
||||
|
||||
public ValueTask ExecuteAsync(IConsole console)
|
||||
{
|
||||
console.Output.WriteLine($"Hello {Target}!");
|
||||
|
||||
return default;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,28 @@
|
||||
using System.Threading.Tasks;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CliFx.Tests.Dummy
|
||||
namespace CliFx.Tests.Dummy;
|
||||
|
||||
// This dummy application is used in tests for scenarios that require an external process to properly verify
|
||||
public static class Program
|
||||
{
|
||||
public class Program
|
||||
// Path to the apphost
|
||||
public static string FilePath { get; } =
|
||||
Path.ChangeExtension(
|
||||
Assembly.GetExecutingAssembly().Location,
|
||||
OperatingSystem.IsWindows() ? "exe" : null
|
||||
);
|
||||
|
||||
public static async Task Main()
|
||||
{
|
||||
public static async Task Main() =>
|
||||
await new CliApplicationBuilder()
|
||||
.AddCommandsFromThisAssembly()
|
||||
.Build()
|
||||
.RunAsync();
|
||||
// Make sure color codes are not produced because we rely on the output in tests
|
||||
Environment.SetEnvironmentVariable(
|
||||
"DOTNET_SYSTEM_CONSOLE_ALLOW_ANSI_COLOR_REDIRECTION",
|
||||
"false"
|
||||
);
|
||||
|
||||
await new CliApplicationBuilder().AddCommandsFromThisAssembly().Build().RunAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
71
CliFx.Tests/ApplicationSpecs.cs
Normal file
71
CliFx.Tests/ApplicationSpecs.cs
Normal file
@@ -0,0 +1,71 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using CliFx.Tests.Utils;
|
||||
using FluentAssertions;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace CliFx.Tests;
|
||||
|
||||
public class ApplicationSpecs(ITestOutputHelper testOutput) : SpecsBase(testOutput)
|
||||
{
|
||||
[Fact]
|
||||
public async Task I_can_create_an_application_with_the_default_configuration()
|
||||
{
|
||||
// Act
|
||||
var app = new CliApplicationBuilder()
|
||||
.AddCommandsFromThisAssembly()
|
||||
.UseConsole(FakeConsole)
|
||||
.Build();
|
||||
|
||||
var exitCode = await app.RunAsync([], new Dictionary<string, string>());
|
||||
|
||||
// Assert
|
||||
exitCode.Should().Be(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task I_can_create_an_application_with_a_custom_configuration()
|
||||
{
|
||||
// Act
|
||||
var app = new CliApplicationBuilder()
|
||||
.AddCommand<NoOpCommand>()
|
||||
.AddCommandsFrom(typeof(NoOpCommand).Assembly)
|
||||
.AddCommands([typeof(NoOpCommand)])
|
||||
.AddCommandsFrom([typeof(NoOpCommand).Assembly])
|
||||
.AddCommandsFromThisAssembly()
|
||||
.AllowDebugMode()
|
||||
.AllowPreviewMode()
|
||||
.SetTitle("test")
|
||||
.SetExecutableName("test")
|
||||
.SetVersion("test")
|
||||
.SetDescription("test")
|
||||
.UseConsole(FakeConsole)
|
||||
.UseTypeActivator(Activator.CreateInstance!)
|
||||
.Build();
|
||||
|
||||
var exitCode = await app.RunAsync([], new Dictionary<string, string>());
|
||||
|
||||
// Assert
|
||||
exitCode.Should().Be(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task I_can_try_to_create_an_application_and_get_an_error_if_it_has_invalid_commands()
|
||||
{
|
||||
// Act
|
||||
var app = new CliApplicationBuilder()
|
||||
.AddCommand(typeof(ApplicationSpecs))
|
||||
.UseConsole(FakeConsole)
|
||||
.Build();
|
||||
|
||||
var exitCode = await app.RunAsync([], new Dictionary<string, string>());
|
||||
|
||||
// Assert
|
||||
exitCode.Should().NotBe(0);
|
||||
|
||||
var stdErr = FakeConsole.ReadErrorString();
|
||||
stdErr.Should().Contain("not a valid command");
|
||||
}
|
||||
}
|
||||
107
CliFx.Tests/CancellationSpecs.cs
Normal file
107
CliFx.Tests/CancellationSpecs.cs
Normal file
@@ -0,0 +1,107 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using CliFx.Tests.Utils;
|
||||
using CliFx.Tests.Utils.Extensions;
|
||||
using CliWrap;
|
||||
using FluentAssertions;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace CliFx.Tests;
|
||||
|
||||
public class CancellationSpecs(ITestOutputHelper testOutput) : SpecsBase(testOutput)
|
||||
{
|
||||
[Fact(Timeout = 15000)]
|
||||
public async Task I_can_configure_the_command_to_listen_to_the_interrupt_signal()
|
||||
{
|
||||
// Arrange
|
||||
using var cts = new CancellationTokenSource();
|
||||
|
||||
// We need to send the cancellation request right after the process has registered
|
||||
// a handler for the interrupt signal, otherwise the default handler will trigger
|
||||
// and just kill the process.
|
||||
void HandleStdOut(string line)
|
||||
{
|
||||
if (string.Equals(line, "Started.", StringComparison.OrdinalIgnoreCase))
|
||||
cts.CancelAfter(TimeSpan.FromSeconds(0.2));
|
||||
}
|
||||
|
||||
var stdOutBuffer = new StringBuilder();
|
||||
|
||||
var pipeTarget = PipeTarget.Merge(
|
||||
PipeTarget.ToDelegate(HandleStdOut),
|
||||
PipeTarget.ToStringBuilder(stdOutBuffer)
|
||||
);
|
||||
|
||||
var command = Cli.Wrap(Dummy.Program.FilePath).WithArguments("cancel-test") | pipeTarget;
|
||||
|
||||
// Act & assert
|
||||
await Assert.ThrowsAnyAsync<OperationCanceledException>(
|
||||
async () =>
|
||||
await command.ExecuteAsync(
|
||||
// Forceful cancellation (not required because we have a timeout)
|
||||
CancellationToken.None,
|
||||
// Graceful cancellation
|
||||
cts.Token
|
||||
)
|
||||
);
|
||||
|
||||
stdOutBuffer.ToString().Trim().Should().ConsistOfLines("Started.", "Cancelled.");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task I_can_configure_the_command_to_listen_to_the_interrupt_signal_when_running_in_isolation()
|
||||
{
|
||||
// Arrange
|
||||
var commandType = DynamicCommandBuilder.Compile(
|
||||
// lang=csharp
|
||||
"""
|
||||
[Command]
|
||||
public class Command : ICommand
|
||||
{
|
||||
public async ValueTask ExecuteAsync(IConsole console)
|
||||
{
|
||||
try
|
||||
{
|
||||
console.WriteLine("Started.");
|
||||
|
||||
await Task.Delay(
|
||||
TimeSpan.FromSeconds(3),
|
||||
console.RegisterCancellationHandler()
|
||||
);
|
||||
|
||||
console.WriteLine("Completed.");
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
console.WriteLine("Cancelled.");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
"""
|
||||
);
|
||||
|
||||
var application = new CliApplicationBuilder()
|
||||
.AddCommand(commandType)
|
||||
.UseConsole(FakeConsole)
|
||||
.Build();
|
||||
|
||||
FakeConsole.RequestCancellation(TimeSpan.FromSeconds(0.2));
|
||||
|
||||
// Act
|
||||
var exitCode = await application.RunAsync(
|
||||
[],
|
||||
new Dictionary<string, string>()
|
||||
);
|
||||
|
||||
// Assert
|
||||
exitCode.Should().NotBe(0);
|
||||
|
||||
var stdOut = FakeConsole.ReadOutputString();
|
||||
stdOut.Trim().Should().ConsistOfLines("Started.", "Cancelled.");
|
||||
}
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
using NUnit.Framework;
|
||||
using System;
|
||||
using System.IO;
|
||||
using CliFx.Tests.TestCommands;
|
||||
|
||||
namespace CliFx.Tests
|
||||
{
|
||||
[TestFixture]
|
||||
public class CliApplicationBuilderTests
|
||||
{
|
||||
[Test(Description = "All builder methods must return without exceptions")]
|
||||
public void Smoke_Test()
|
||||
{
|
||||
// Arrange
|
||||
var builder = new CliApplicationBuilder();
|
||||
|
||||
// Act
|
||||
builder
|
||||
.AddCommand(typeof(HelloWorldDefaultCommand))
|
||||
.AddCommandsFrom(typeof(HelloWorldDefaultCommand).Assembly)
|
||||
.AddCommands(new[] {typeof(HelloWorldDefaultCommand)})
|
||||
.AddCommandsFrom(new[] {typeof(HelloWorldDefaultCommand).Assembly})
|
||||
.AddCommandsFromThisAssembly()
|
||||
.AllowDebugMode()
|
||||
.AllowPreviewMode()
|
||||
.UseTitle("test")
|
||||
.UseExecutableName("test")
|
||||
.UseVersionText("test")
|
||||
.UseDescription("test")
|
||||
.UseConsole(new VirtualConsole(TextWriter.Null))
|
||||
.UseTypeActivator(Activator.CreateInstance)
|
||||
.Build();
|
||||
}
|
||||
|
||||
[Test(Description = "Builder must be able to produce an application when no parameters are specified")]
|
||||
public void Build_Test()
|
||||
{
|
||||
// Arrange
|
||||
var builder = new CliApplicationBuilder();
|
||||
|
||||
// Act
|
||||
builder.Build();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,451 +0,0 @@
|
||||
using FluentAssertions;
|
||||
using NUnit.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using CliFx.Tests.TestCommands;
|
||||
|
||||
namespace CliFx.Tests
|
||||
{
|
||||
[TestFixture]
|
||||
public class CliApplicationTests
|
||||
{
|
||||
private const string TestAppName = "TestApp";
|
||||
private const string TestVersionText = "v1.0";
|
||||
|
||||
private static IEnumerable<TestCaseData> GetTestCases_RunAsync()
|
||||
{
|
||||
yield return new TestCaseData(
|
||||
new[] {typeof(HelloWorldDefaultCommand)},
|
||||
new string[0],
|
||||
new Dictionary<string, string>(),
|
||||
"Hello world."
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {typeof(ConcatCommand)},
|
||||
new[] {"concat", "-i", "foo", "-i", "bar", "-s", " "},
|
||||
new Dictionary<string, string>(),
|
||||
"foo bar"
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {typeof(ConcatCommand)},
|
||||
new[] {"concat", "-i", "one", "two", "three", "-s", ", "},
|
||||
new Dictionary<string, string>(),
|
||||
"one, two, three"
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {typeof(DivideCommand)},
|
||||
new[] {"div", "-D", "24", "-d", "8"},
|
||||
new Dictionary<string, string>(),
|
||||
"3"
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {typeof(HelloWorldDefaultCommand)},
|
||||
new[] {"--version"},
|
||||
new Dictionary<string, string>(),
|
||||
TestVersionText
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {typeof(ConcatCommand)},
|
||||
new[] {"--version"},
|
||||
new Dictionary<string, string>(),
|
||||
TestVersionText
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {typeof(ConcatCommand)},
|
||||
new string[0],
|
||||
new Dictionary<string, string>(),
|
||||
null
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {typeof(ConcatCommand)},
|
||||
new[] {"-h"},
|
||||
new Dictionary<string, string>(),
|
||||
null
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {typeof(ConcatCommand)},
|
||||
new[] {"--help"},
|
||||
new Dictionary<string, string>(),
|
||||
null
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {typeof(ConcatCommand)},
|
||||
new[] {"concat", "-h"},
|
||||
new Dictionary<string, string>(),
|
||||
null
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {typeof(ExceptionCommand)},
|
||||
new[] {"exc", "-h"},
|
||||
new Dictionary<string, string>(),
|
||||
null
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {typeof(CommandExceptionCommand)},
|
||||
new[] {"exc", "-h"},
|
||||
new Dictionary<string, string>(),
|
||||
null
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {typeof(ConcatCommand)},
|
||||
new[] {"[preview]"},
|
||||
new Dictionary<string, string>(),
|
||||
null
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {typeof(ExceptionCommand)},
|
||||
new[] {"[preview]", "exc"},
|
||||
new Dictionary<string, string>(),
|
||||
null
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {typeof(ConcatCommand)},
|
||||
new[] {"[preview]", "concat", "-o", "value"},
|
||||
new Dictionary<string, string>(),
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
private static IEnumerable<TestCaseData> GetTestCases_RunAsync_Negative()
|
||||
{
|
||||
yield return new TestCaseData(
|
||||
new Type[0],
|
||||
new string[0],
|
||||
new Dictionary<string, string>(),
|
||||
null, null
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {typeof(ConcatCommand)},
|
||||
new[] {"non-existing"},
|
||||
new Dictionary<string, string>(),
|
||||
null, null
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {typeof(ExceptionCommand)},
|
||||
new[] {"exc"},
|
||||
new Dictionary<string, string>(),
|
||||
null, null
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {typeof(CommandExceptionCommand)},
|
||||
new[] {"exc"},
|
||||
new Dictionary<string, string>(),
|
||||
null, null
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {typeof(CommandExceptionCommand)},
|
||||
new[] {"exc"},
|
||||
new Dictionary<string, string>(),
|
||||
null, null
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {typeof(CommandExceptionCommand)},
|
||||
new[] {"exc", "-m", "foo bar"},
|
||||
new Dictionary<string, string>(),
|
||||
"foo bar", null
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {typeof(CommandExceptionCommand)},
|
||||
new[] {"exc", "-m", "foo bar", "-c", "666"},
|
||||
new Dictionary<string, string>(),
|
||||
"foo bar", 666
|
||||
);
|
||||
}
|
||||
|
||||
private static IEnumerable<TestCaseData> GetTestCases_RunAsync_Help()
|
||||
{
|
||||
yield return new TestCaseData(
|
||||
new[] {typeof(HelpDefaultCommand), typeof(HelpNamedCommand), typeof(HelpSubCommand)},
|
||||
new[] {"--help"},
|
||||
new[]
|
||||
{
|
||||
TestVersionText,
|
||||
"Description",
|
||||
"HelpDefaultCommand description.",
|
||||
"Usage",
|
||||
TestAppName, "[command]", "[options]",
|
||||
"Options",
|
||||
"-a|--option-a", "OptionA description.",
|
||||
"-b|--option-b", "OptionB description.",
|
||||
"-h|--help", "Shows help text.",
|
||||
"--version", "Shows version information.",
|
||||
"Commands",
|
||||
"cmd", "HelpNamedCommand description.",
|
||||
"You can run", "to show help on a specific command."
|
||||
}
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {typeof(HelpSubCommand)},
|
||||
new[] {"--help"},
|
||||
new[]
|
||||
{
|
||||
TestVersionText,
|
||||
"Usage",
|
||||
TestAppName, "[command]",
|
||||
"Options",
|
||||
"-h|--help", "Shows help text.",
|
||||
"--version", "Shows version information.",
|
||||
"Commands",
|
||||
"cmd sub", "HelpSubCommand description.",
|
||||
"You can run", "to show help on a specific command."
|
||||
}
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {typeof(HelpDefaultCommand), typeof(HelpNamedCommand), typeof(HelpSubCommand)},
|
||||
new[] {"cmd", "--help"},
|
||||
new[]
|
||||
{
|
||||
"Description",
|
||||
"HelpNamedCommand description.",
|
||||
"Usage",
|
||||
TestAppName, "cmd", "[command]", "[options]",
|
||||
"Options",
|
||||
"-c|--option-c", "OptionC description.",
|
||||
"-d|--option-d", "OptionD description.",
|
||||
"-h|--help", "Shows help text.",
|
||||
"Commands",
|
||||
"sub", "HelpSubCommand description.",
|
||||
"You can run", "to show help on a specific command."
|
||||
}
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {typeof(HelpDefaultCommand), typeof(HelpNamedCommand), typeof(HelpSubCommand)},
|
||||
new[] {"cmd", "sub", "--help"},
|
||||
new[]
|
||||
{
|
||||
"Description",
|
||||
"HelpSubCommand description.",
|
||||
"Usage",
|
||||
TestAppName, "cmd sub", "[options]",
|
||||
"Options",
|
||||
"-e|--option-e", "OptionE description.",
|
||||
"-h|--help", "Shows help text."
|
||||
}
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {typeof(ParameterCommand)},
|
||||
new[] {"param", "cmd", "--help"},
|
||||
new[]
|
||||
{
|
||||
"Description",
|
||||
"Command using positional parameters",
|
||||
"Usage",
|
||||
TestAppName, "param cmd", "<first>", "<parameterb>", "<third list...>", "[options]",
|
||||
"Parameters",
|
||||
"* first",
|
||||
"* parameterb",
|
||||
"* third list", "A list of numbers",
|
||||
"Options",
|
||||
"-o|--option",
|
||||
"-h|--help", "Shows help text."
|
||||
}
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {typeof(AllRequiredOptionsCommand)},
|
||||
new[] {"allrequired", "--help"},
|
||||
new[]
|
||||
{
|
||||
"Description",
|
||||
"AllRequiredOptionsCommand description.",
|
||||
"Usage",
|
||||
TestAppName, "allrequired --option-f <value> --option-g <value>"
|
||||
}
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {typeof(SomeRequiredOptionsCommand)},
|
||||
new[] {"somerequired", "--help"},
|
||||
new[]
|
||||
{
|
||||
"Description",
|
||||
"SomeRequiredOptionsCommand description.",
|
||||
"Usage",
|
||||
TestAppName, "somerequired --option-f <value> [options]"
|
||||
}
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {typeof(EnvironmentVariableCommand)},
|
||||
new[] {"--help"},
|
||||
new[]
|
||||
{
|
||||
"Environment variable:", "ENV_SINGLE_VALUE"
|
||||
}
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {typeof(ConcatCommand)},
|
||||
new[] {"concat", "--help"},
|
||||
new[]
|
||||
{
|
||||
"Usage",
|
||||
TestAppName, "concat", "-i", "<values...>", "[options]",
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
[TestCaseSource(nameof(GetTestCases_RunAsync))]
|
||||
public async Task RunAsync_Test(
|
||||
IReadOnlyList<Type> commandTypes,
|
||||
IReadOnlyList<string> commandLineArguments,
|
||||
IReadOnlyDictionary<string, string> environmentVariables,
|
||||
string? expectedStdOut = null)
|
||||
{
|
||||
// Arrange
|
||||
await using var stdOutStream = new StringWriter();
|
||||
var console = new VirtualConsole(stdOutStream);
|
||||
|
||||
var application = new CliApplicationBuilder()
|
||||
.AddCommands(commandTypes)
|
||||
.UseTitle(TestAppName)
|
||||
.UseExecutableName(TestAppName)
|
||||
.UseVersionText(TestVersionText)
|
||||
.UseConsole(console)
|
||||
.Build();
|
||||
|
||||
// Act
|
||||
var exitCode = await application.RunAsync(commandLineArguments, environmentVariables);
|
||||
var stdOut = stdOutStream.ToString().Trim();
|
||||
|
||||
// Assert
|
||||
exitCode.Should().Be(0);
|
||||
stdOut.Should().NotBeNullOrWhiteSpace();
|
||||
|
||||
if (expectedStdOut != null)
|
||||
stdOut.Should().Be(expectedStdOut);
|
||||
|
||||
Console.WriteLine(stdOut);
|
||||
}
|
||||
|
||||
[TestCaseSource(nameof(GetTestCases_RunAsync_Negative))]
|
||||
public async Task RunAsync_Negative_Test(
|
||||
IReadOnlyList<Type> commandTypes,
|
||||
IReadOnlyList<string> commandLineArguments,
|
||||
IReadOnlyDictionary<string, string> environmentVariables,
|
||||
string? expectedStdErr = null,
|
||||
int? expectedExitCode = null)
|
||||
{
|
||||
// Arrange
|
||||
await using var stdErrStream = new StringWriter();
|
||||
var console = new VirtualConsole(TextWriter.Null, stdErrStream);
|
||||
|
||||
var application = new CliApplicationBuilder()
|
||||
.AddCommands(commandTypes)
|
||||
.UseTitle(TestAppName)
|
||||
.UseExecutableName(TestAppName)
|
||||
.UseVersionText(TestVersionText)
|
||||
.UseConsole(console)
|
||||
.Build();
|
||||
|
||||
// Act
|
||||
var exitCode = await application.RunAsync(commandLineArguments, environmentVariables);
|
||||
var stderr = stdErrStream.ToString().Trim();
|
||||
|
||||
// Assert
|
||||
exitCode.Should().NotBe(0);
|
||||
stderr.Should().NotBeNullOrWhiteSpace();
|
||||
|
||||
if (expectedExitCode != null)
|
||||
exitCode.Should().Be(expectedExitCode);
|
||||
|
||||
if (expectedStdErr != null)
|
||||
stderr.Should().Be(expectedStdErr);
|
||||
|
||||
Console.WriteLine(stderr);
|
||||
}
|
||||
|
||||
[TestCaseSource(nameof(GetTestCases_RunAsync_Help))]
|
||||
public async Task RunAsync_Help_Test(
|
||||
IReadOnlyList<Type> commandTypes,
|
||||
IReadOnlyList<string> commandLineArguments,
|
||||
IReadOnlyList<string>? expectedSubstrings = null)
|
||||
{
|
||||
// Arrange
|
||||
await using var stdOutStream = new StringWriter();
|
||||
var console = new VirtualConsole(stdOutStream);
|
||||
|
||||
var application = new CliApplicationBuilder()
|
||||
.AddCommands(commandTypes)
|
||||
.UseTitle(TestAppName)
|
||||
.UseExecutableName(TestAppName)
|
||||
.UseVersionText(TestVersionText)
|
||||
.UseConsole(console)
|
||||
.Build();
|
||||
|
||||
var environmentVariables = new Dictionary<string, string>();
|
||||
|
||||
// Act
|
||||
var exitCode = await application.RunAsync(commandLineArguments, environmentVariables);
|
||||
var stdOut = stdOutStream.ToString().Trim();
|
||||
|
||||
// Assert
|
||||
exitCode.Should().Be(0);
|
||||
stdOut.Should().NotBeNullOrWhiteSpace();
|
||||
|
||||
if (expectedSubstrings != null)
|
||||
stdOut.Should().ContainAll(expectedSubstrings);
|
||||
|
||||
Console.WriteLine(stdOut);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task RunAsync_Cancellation_Test()
|
||||
{
|
||||
// Arrange
|
||||
using var cancellationTokenSource = new CancellationTokenSource();
|
||||
|
||||
await using var stdOutStream = new StringWriter();
|
||||
await using var stdErrStream = new StringWriter();
|
||||
var console = new VirtualConsole(stdOutStream, stdErrStream, cancellationTokenSource.Token);
|
||||
|
||||
var application = new CliApplicationBuilder()
|
||||
.AddCommand(typeof(CancellableCommand))
|
||||
.UseConsole(console)
|
||||
.Build();
|
||||
|
||||
var commandLineArguments = new[] {"cancel"};
|
||||
var environmentVariables = new Dictionary<string, string>();
|
||||
|
||||
// Act
|
||||
cancellationTokenSource.CancelAfter(TimeSpan.FromSeconds(0.2));
|
||||
var exitCode = await application.RunAsync(commandLineArguments, environmentVariables);
|
||||
var stdOut = stdOutStream.ToString().Trim();
|
||||
var stdErr = stdErrStream.ToString().Trim();
|
||||
|
||||
// Assert
|
||||
exitCode.Should().NotBe(0);
|
||||
stdOut.Should().BeNullOrWhiteSpace();
|
||||
stdErr.Should().NotBeNullOrWhiteSpace();
|
||||
|
||||
Console.WriteLine(stdErr);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,22 +1,26 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Import Project="../CliFx.props" />
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netcoreapp3.1</TargetFramework>
|
||||
<IsPackable>false</IsPackable>
|
||||
<IsTestProject>true</IsTestProject>
|
||||
<CollectCoverage>true</CollectCoverage>
|
||||
<CoverletOutputFormat>opencover</CoverletOutputFormat>
|
||||
<CoverletOutput>bin/$(Configuration)/Coverage.xml</CoverletOutput>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="CliWrap" Version="2.5.0" />
|
||||
<PackageReference Include="FluentAssertions" Version="5.10.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.4.0" />
|
||||
<PackageReference Include="NUnit" Version="3.12.0" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="3.16.1" />
|
||||
<PackageReference Include="coverlet.msbuild" Version="2.8.0" PrivateAssets="all" />
|
||||
<Content Include="xunit.runner.json" CopyToOutputDirectory="PreserveNewest" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Basic.Reference.Assemblies.Net80" Version="1.8.3" />
|
||||
<PackageReference Include="CliWrap" Version="3.9.0" />
|
||||
<PackageReference Include="coverlet.collector" Version="6.0.4" PrivateAssets="all" />
|
||||
<PackageReference Include="CSharpier.MsBuild" Version="1.1.2" PrivateAssets="all" />
|
||||
<PackageReference Include="FluentAssertions" Version="8.6.0" />
|
||||
<PackageReference Include="GitHubActionsTestLogger" Version="2.4.1" PrivateAssets="all" />
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.14.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.9" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
|
||||
<PackageReference Include="PolyShim" Version="1.15.0" PrivateAssets="all" />
|
||||
<PackageReference Include="xunit" Version="2.9.3" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.4" PrivateAssets="all" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
@@ -24,8 +28,4 @@
|
||||
<ProjectReference Include="..\CliFx\CliFx.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<Target Name="Copy dummy's runtime config" AfterTargets="AfterBuild">
|
||||
<Copy SourceFiles="../CliFx.Tests.Dummy/bin/$(Configuration)/$(TargetFramework)/CliFx.Tests.Dummy.runtimeconfig.json" DestinationFiles="$(OutputPath)CliFx.Tests.Dummy.runtimeconfig.json" />
|
||||
</Target>
|
||||
|
||||
</Project>
|
||||
204
CliFx.Tests/ConsoleSpecs.cs
Normal file
204
CliFx.Tests/ConsoleSpecs.cs
Normal file
@@ -0,0 +1,204 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using CliFx.Infrastructure;
|
||||
using CliFx.Tests.Utils;
|
||||
using CliFx.Tests.Utils.Extensions;
|
||||
using CliWrap;
|
||||
using CliWrap.Buffered;
|
||||
using FluentAssertions;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace CliFx.Tests;
|
||||
|
||||
public class ConsoleSpecs(ITestOutputHelper testOutput) : SpecsBase(testOutput)
|
||||
{
|
||||
[Fact(Timeout = 15000)]
|
||||
public async Task I_can_run_the_application_with_the_default_console_implementation_to_interact_with_the_system_console()
|
||||
{
|
||||
// Can't verify our own console output, so using an external process for this test
|
||||
|
||||
// Arrange
|
||||
var command =
|
||||
"Hello world" | Cli.Wrap(Dummy.Program.FilePath).WithArguments("console-test");
|
||||
|
||||
// Act
|
||||
var result = await command.ExecuteBufferedAsync();
|
||||
|
||||
// Assert
|
||||
result.StandardOutput.Trim().Should().Be("Hello world");
|
||||
result.StandardError.Trim().Should().Be("Hello world");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void I_can_run_the_application_on_a_system_with_a_custom_console_encoding_and_not_get_corrupted_output()
|
||||
{
|
||||
// Arrange
|
||||
using var buffer = new MemoryStream();
|
||||
using var consoleWriter = new ConsoleWriter(FakeConsole, buffer, Encoding.UTF8);
|
||||
|
||||
// Act
|
||||
consoleWriter.Write("Hello world");
|
||||
consoleWriter.Flush();
|
||||
|
||||
// Assert
|
||||
var outputBytes = buffer.ToArray();
|
||||
outputBytes.Should().NotContain(Encoding.UTF8.GetPreamble());
|
||||
|
||||
var output = consoleWriter.Encoding.GetString(outputBytes);
|
||||
output.Should().Be("Hello world");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task I_can_run_the_application_with_the_fake_console_implementation_to_isolate_console_interactions()
|
||||
{
|
||||
// Arrange
|
||||
var commandType = DynamicCommandBuilder.Compile(
|
||||
// lang=csharp
|
||||
"""
|
||||
[Command]
|
||||
public class Command : ICommand
|
||||
{
|
||||
public ValueTask ExecuteAsync(IConsole console)
|
||||
{
|
||||
console.ResetColor();
|
||||
console.ForegroundColor = ConsoleColor.DarkMagenta;
|
||||
console.BackgroundColor = ConsoleColor.DarkMagenta;
|
||||
console.WindowWidth = 100;
|
||||
console.WindowHeight = 25;
|
||||
console.CursorLeft = 42;
|
||||
console.CursorTop = 24;
|
||||
|
||||
console.Output.WriteLine("Hello ");
|
||||
console.Error.WriteLine("world!");
|
||||
|
||||
return default;
|
||||
}
|
||||
}
|
||||
"""
|
||||
);
|
||||
|
||||
var application = new CliApplicationBuilder()
|
||||
.AddCommand(commandType)
|
||||
.UseConsole(FakeConsole)
|
||||
.Build();
|
||||
|
||||
// Act
|
||||
var exitCode = await application.RunAsync(
|
||||
[],
|
||||
new Dictionary<string, string>()
|
||||
);
|
||||
|
||||
// Assert
|
||||
exitCode.Should().Be(0);
|
||||
|
||||
Console.OpenStandardInput().Should().NotBeSameAs(FakeConsole.Input.BaseStream);
|
||||
Console.OpenStandardOutput().Should().NotBeSameAs(FakeConsole.Output.BaseStream);
|
||||
Console.OpenStandardError().Should().NotBeSameAs(FakeConsole.Error.BaseStream);
|
||||
|
||||
Console.ForegroundColor.Should().NotBe(ConsoleColor.DarkMagenta);
|
||||
Console.BackgroundColor.Should().NotBe(ConsoleColor.DarkMagenta);
|
||||
|
||||
// This fails because tests don't spawn a console window
|
||||
//Console.WindowWidth.Should().Be(100);
|
||||
//Console.WindowHeight.Should().Be(25);
|
||||
//Console.CursorLeft.Should().NotBe(42);
|
||||
//Console.CursorTop.Should().NotBe(24);
|
||||
|
||||
FakeConsole.IsInputRedirected.Should().BeTrue();
|
||||
FakeConsole.IsOutputRedirected.Should().BeTrue();
|
||||
FakeConsole.IsErrorRedirected.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task I_can_run_the_application_with_the_fake_console_implementation_and_simulate_stream_interactions()
|
||||
{
|
||||
// Arrange
|
||||
var commandType = DynamicCommandBuilder.Compile(
|
||||
// lang=csharp
|
||||
"""
|
||||
[Command]
|
||||
public class Command : ICommand
|
||||
{
|
||||
public ValueTask ExecuteAsync(IConsole console)
|
||||
{
|
||||
var input = console.Input.ReadToEnd();
|
||||
console.Output.WriteLine(input);
|
||||
console.Error.WriteLine(input);
|
||||
|
||||
return default;
|
||||
}
|
||||
}
|
||||
"""
|
||||
);
|
||||
|
||||
var application = new CliApplicationBuilder()
|
||||
.AddCommand(commandType)
|
||||
.UseConsole(FakeConsole)
|
||||
.Build();
|
||||
|
||||
// Act
|
||||
FakeConsole.WriteInput("Hello world");
|
||||
|
||||
var exitCode = await application.RunAsync(
|
||||
[],
|
||||
new Dictionary<string, string>()
|
||||
);
|
||||
|
||||
// Assert
|
||||
exitCode.Should().Be(0);
|
||||
|
||||
var stdOut = FakeConsole.ReadOutputString();
|
||||
stdOut.Trim().Should().Be("Hello world");
|
||||
|
||||
var stdErr = FakeConsole.ReadErrorString();
|
||||
stdErr.Trim().Should().Be("Hello world");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task I_can_run_the_application_with_the_fake_console_implementation_and_simulate_key_presses()
|
||||
{
|
||||
// Arrange
|
||||
var commandType = DynamicCommandBuilder.Compile(
|
||||
// lang=csharp
|
||||
"""
|
||||
[Command]
|
||||
public class Command : ICommand
|
||||
{
|
||||
public ValueTask ExecuteAsync(IConsole console)
|
||||
{
|
||||
console.WriteLine(console.ReadKey().Key);
|
||||
console.WriteLine(console.ReadKey().Key);
|
||||
console.WriteLine(console.ReadKey().Key);
|
||||
|
||||
return default;
|
||||
}
|
||||
}
|
||||
"""
|
||||
);
|
||||
|
||||
var application = new CliApplicationBuilder()
|
||||
.AddCommand(commandType)
|
||||
.UseConsole(FakeConsole)
|
||||
.Build();
|
||||
|
||||
// Act
|
||||
FakeConsole.EnqueueKey(new ConsoleKeyInfo('0', ConsoleKey.D0, false, false, false));
|
||||
FakeConsole.EnqueueKey(new ConsoleKeyInfo('a', ConsoleKey.A, false, false, false));
|
||||
FakeConsole.EnqueueKey(new ConsoleKeyInfo('\0', ConsoleKey.Backspace, false, false, false));
|
||||
|
||||
var exitCode = await application.RunAsync(
|
||||
[],
|
||||
new Dictionary<string, string>()
|
||||
);
|
||||
|
||||
// Assert
|
||||
exitCode.Should().Be(0);
|
||||
|
||||
var stdOut = FakeConsole.ReadOutputString();
|
||||
stdOut.Trim().Should().ConsistOfLines("D0", "A", "Backspace");
|
||||
}
|
||||
}
|
||||
941
CliFx.Tests/ConversionSpecs.cs
Normal file
941
CliFx.Tests/ConversionSpecs.cs
Normal file
@@ -0,0 +1,941 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using CliFx.Tests.Utils;
|
||||
using CliFx.Tests.Utils.Extensions;
|
||||
using FluentAssertions;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace CliFx.Tests;
|
||||
|
||||
public class ConversionSpecs(ITestOutputHelper testOutput) : SpecsBase(testOutput)
|
||||
{
|
||||
[Fact]
|
||||
public async Task I_can_bind_a_parameter_or_an_option_to_a_string_property()
|
||||
{
|
||||
// Arrange
|
||||
var commandType = DynamicCommandBuilder.Compile(
|
||||
// lang=csharp
|
||||
"""
|
||||
[Command]
|
||||
public class Command : ICommand
|
||||
{
|
||||
[CommandOption('f')]
|
||||
public string? Foo { get; init; }
|
||||
|
||||
public ValueTask ExecuteAsync(IConsole console)
|
||||
{
|
||||
console.WriteLine(Foo);
|
||||
return default;
|
||||
}
|
||||
}
|
||||
"""
|
||||
);
|
||||
|
||||
var application = new CliApplicationBuilder()
|
||||
.AddCommand(commandType)
|
||||
.UseConsole(FakeConsole)
|
||||
.Build();
|
||||
|
||||
// Act
|
||||
var exitCode = await application.RunAsync(["-f", "xyz"], new Dictionary<string, string>());
|
||||
|
||||
// Assert
|
||||
exitCode.Should().Be(0);
|
||||
|
||||
var stdOut = FakeConsole.ReadOutputString();
|
||||
stdOut.Trim().Should().Be("xyz");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task I_can_bind_a_parameter_or_an_option_to_an_object_property()
|
||||
{
|
||||
// Arrange
|
||||
var commandType = DynamicCommandBuilder.Compile(
|
||||
// lang=csharp
|
||||
"""
|
||||
[Command]
|
||||
public class Command : ICommand
|
||||
{
|
||||
[CommandOption('f')]
|
||||
public object? Foo { get; init; }
|
||||
|
||||
public ValueTask ExecuteAsync(IConsole console)
|
||||
{
|
||||
console.WriteLine(Foo);
|
||||
return default;
|
||||
}
|
||||
}
|
||||
"""
|
||||
);
|
||||
|
||||
var application = new CliApplicationBuilder()
|
||||
.AddCommand(commandType)
|
||||
.UseConsole(FakeConsole)
|
||||
.Build();
|
||||
|
||||
// Act
|
||||
var exitCode = await application.RunAsync(["-f", "xyz"], new Dictionary<string, string>());
|
||||
|
||||
// Assert
|
||||
exitCode.Should().Be(0);
|
||||
|
||||
var stdOut = FakeConsole.ReadOutputString();
|
||||
stdOut.Trim().Should().Be("xyz");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task I_can_bind_a_parameter_or_an_option_to_a_boolean_property()
|
||||
{
|
||||
// Arrange
|
||||
var commandType = DynamicCommandBuilder.Compile(
|
||||
// lang=csharp
|
||||
"""
|
||||
[Command]
|
||||
public class Command : ICommand
|
||||
{
|
||||
[CommandOption('f')]
|
||||
public bool Foo { get; init; }
|
||||
|
||||
[CommandOption('b')]
|
||||
public bool Bar { get; init; }
|
||||
|
||||
[CommandOption('c')]
|
||||
public bool Baz { get; init; }
|
||||
|
||||
public ValueTask ExecuteAsync(IConsole console)
|
||||
{
|
||||
console.WriteLine("Foo = " + Foo);
|
||||
console.WriteLine("Bar = " + Bar);
|
||||
console.WriteLine("Baz = " + Baz);
|
||||
|
||||
return default;
|
||||
}
|
||||
}
|
||||
"""
|
||||
);
|
||||
|
||||
var application = new CliApplicationBuilder()
|
||||
.AddCommand(commandType)
|
||||
.UseConsole(FakeConsole)
|
||||
.Build();
|
||||
|
||||
// Act
|
||||
var exitCode = await application.RunAsync(
|
||||
["-f", "true", "-b", "false", "-c"],
|
||||
new Dictionary<string, string>()
|
||||
);
|
||||
|
||||
// Assert
|
||||
exitCode.Should().Be(0);
|
||||
|
||||
var stdOut = FakeConsole.ReadOutputString();
|
||||
stdOut.Should().ConsistOfLines("Foo = True", "Bar = False", "Baz = True");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task I_can_bind_a_parameter_or_an_option_to_an_integer_property()
|
||||
{
|
||||
// Arrange
|
||||
var commandType = DynamicCommandBuilder.Compile(
|
||||
// lang=csharp
|
||||
"""
|
||||
[Command]
|
||||
public class Command : ICommand
|
||||
{
|
||||
[CommandOption('f')]
|
||||
public int Foo { get; init; }
|
||||
|
||||
public ValueTask ExecuteAsync(IConsole console)
|
||||
{
|
||||
console.WriteLine(Foo);
|
||||
return default;
|
||||
}
|
||||
}
|
||||
"""
|
||||
);
|
||||
|
||||
var application = new CliApplicationBuilder()
|
||||
.AddCommand(commandType)
|
||||
.UseConsole(FakeConsole)
|
||||
.Build();
|
||||
|
||||
// Act
|
||||
var exitCode = await application.RunAsync(["-f", "32"], new Dictionary<string, string>());
|
||||
|
||||
// Assert
|
||||
exitCode.Should().Be(0);
|
||||
|
||||
var stdOut = FakeConsole.ReadOutputString();
|
||||
stdOut.Trim().Should().Be("32");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task I_can_bind_a_parameter_or_an_option_to_a_double_property()
|
||||
{
|
||||
// Arrange
|
||||
var commandType = DynamicCommandBuilder.Compile(
|
||||
// lang=csharp
|
||||
"""
|
||||
[Command]
|
||||
public class Command : ICommand
|
||||
{
|
||||
[CommandOption('f')]
|
||||
public double Foo { get; init; }
|
||||
|
||||
public ValueTask ExecuteAsync(IConsole console)
|
||||
{
|
||||
console.WriteLine(Foo.ToString(CultureInfo.InvariantCulture));
|
||||
return default;
|
||||
}
|
||||
}
|
||||
"""
|
||||
);
|
||||
|
||||
var application = new CliApplicationBuilder()
|
||||
.AddCommand(commandType)
|
||||
.UseConsole(FakeConsole)
|
||||
.Build();
|
||||
|
||||
// Act
|
||||
var exitCode = await application.RunAsync(
|
||||
["-f", "32.14"],
|
||||
new Dictionary<string, string>()
|
||||
);
|
||||
|
||||
// Assert
|
||||
exitCode.Should().Be(0);
|
||||
|
||||
var stdOut = FakeConsole.ReadOutputString();
|
||||
stdOut.Trim().Should().Be("32.14");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task I_can_bind_a_parameter_or_an_option_to_a_DateTimeOffset_property()
|
||||
{
|
||||
// Arrange
|
||||
var commandType = DynamicCommandBuilder.Compile(
|
||||
// lang=csharp
|
||||
"""
|
||||
[Command]
|
||||
public class Command : ICommand
|
||||
{
|
||||
[CommandOption('f')]
|
||||
public DateTimeOffset Foo { get; init; }
|
||||
|
||||
public ValueTask ExecuteAsync(IConsole console)
|
||||
{
|
||||
console.WriteLine(Foo.ToString("u", CultureInfo.InvariantCulture));
|
||||
return default;
|
||||
}
|
||||
}
|
||||
"""
|
||||
);
|
||||
|
||||
var application = new CliApplicationBuilder()
|
||||
.AddCommand(commandType)
|
||||
.UseConsole(FakeConsole)
|
||||
.Build();
|
||||
|
||||
// Act
|
||||
var exitCode = await application.RunAsync(
|
||||
["-f", "1995-04-28Z"],
|
||||
new Dictionary<string, string>()
|
||||
);
|
||||
|
||||
// Assert
|
||||
exitCode.Should().Be(0);
|
||||
|
||||
var stdOut = FakeConsole.ReadOutputString();
|
||||
stdOut.Trim().Should().Be("1995-04-28 00:00:00Z");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task I_can_bind_a_parameter_or_an_option_to_a_TimeSpan_property()
|
||||
{
|
||||
// Arrange
|
||||
var commandType = DynamicCommandBuilder.Compile(
|
||||
// lang=csharp
|
||||
"""
|
||||
[Command]
|
||||
public class Command : ICommand
|
||||
{
|
||||
[CommandOption('f')]
|
||||
public TimeSpan Foo { get; init; }
|
||||
|
||||
public ValueTask ExecuteAsync(IConsole console)
|
||||
{
|
||||
console.WriteLine(Foo.ToString(null, CultureInfo.InvariantCulture));
|
||||
return default;
|
||||
}
|
||||
}
|
||||
"""
|
||||
);
|
||||
|
||||
var application = new CliApplicationBuilder()
|
||||
.AddCommand(commandType)
|
||||
.UseConsole(FakeConsole)
|
||||
.Build();
|
||||
|
||||
// Act
|
||||
var exitCode = await application.RunAsync(
|
||||
["-f", "12:34:56"],
|
||||
new Dictionary<string, string>()
|
||||
);
|
||||
|
||||
// Assert
|
||||
exitCode.Should().Be(0);
|
||||
|
||||
var stdOut = FakeConsole.ReadOutputString();
|
||||
stdOut.Trim().Should().Be("12:34:56");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task I_can_bind_a_parameter_or_an_option_to_an_enum_property()
|
||||
{
|
||||
// Arrange
|
||||
var commandType = DynamicCommandBuilder.Compile(
|
||||
// lang=csharp
|
||||
"""
|
||||
public enum CustomEnum { One = 1, Two = 2, Three = 3 }
|
||||
|
||||
[Command]
|
||||
public class Command : ICommand
|
||||
{
|
||||
[CommandOption('f')]
|
||||
public CustomEnum Foo { get; init; }
|
||||
|
||||
public ValueTask ExecuteAsync(IConsole console)
|
||||
{
|
||||
console.WriteLine((int) Foo);
|
||||
return default;
|
||||
}
|
||||
}
|
||||
"""
|
||||
);
|
||||
|
||||
var application = new CliApplicationBuilder()
|
||||
.AddCommand(commandType)
|
||||
.UseConsole(FakeConsole)
|
||||
.Build();
|
||||
|
||||
// Act
|
||||
var exitCode = await application.RunAsync(["-f", "two"], new Dictionary<string, string>());
|
||||
|
||||
// Assert
|
||||
exitCode.Should().Be(0);
|
||||
|
||||
var stdOut = FakeConsole.ReadOutputString();
|
||||
stdOut.Trim().Should().Be("2");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task I_can_bind_a_parameter_or_an_option_to_a_nullable_integer_property()
|
||||
{
|
||||
// Arrange
|
||||
var commandType = DynamicCommandBuilder.Compile(
|
||||
// lang=csharp
|
||||
"""
|
||||
[Command]
|
||||
public class Command : ICommand
|
||||
{
|
||||
[CommandOption('f')]
|
||||
public int? Foo { get; init; }
|
||||
|
||||
[CommandOption('b')]
|
||||
public int? Bar { get; init; }
|
||||
|
||||
public ValueTask ExecuteAsync(IConsole console)
|
||||
{
|
||||
console.WriteLine("Foo = " + Foo);
|
||||
console.WriteLine("Bar = " + Bar);
|
||||
|
||||
return default;
|
||||
}
|
||||
}
|
||||
"""
|
||||
);
|
||||
|
||||
var application = new CliApplicationBuilder()
|
||||
.AddCommand(commandType)
|
||||
.UseConsole(FakeConsole)
|
||||
.Build();
|
||||
|
||||
// Act
|
||||
var exitCode = await application.RunAsync(["-b", "123"], new Dictionary<string, string>());
|
||||
|
||||
// Assert
|
||||
exitCode.Should().Be(0);
|
||||
|
||||
var stdOut = FakeConsole.ReadOutputString();
|
||||
stdOut.Should().ConsistOfLines("Foo = ", "Bar = 123");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task I_can_bind_a_parameter_or_an_option_to_a_nullable_enum_property()
|
||||
{
|
||||
// Arrange
|
||||
var commandType = DynamicCommandBuilder.Compile(
|
||||
// lang=csharp
|
||||
"""
|
||||
public enum CustomEnum { One = 1, Two = 2, Three = 3 }
|
||||
|
||||
[Command]
|
||||
public class Command : ICommand
|
||||
{
|
||||
[CommandOption('f')]
|
||||
public CustomEnum? Foo { get; init; }
|
||||
|
||||
[CommandOption('b')]
|
||||
public CustomEnum? Bar { get; init; }
|
||||
|
||||
public ValueTask ExecuteAsync(IConsole console)
|
||||
{
|
||||
console.WriteLine("Foo = " + (int?) Foo);
|
||||
console.WriteLine("Bar = " + (int?) Bar);
|
||||
|
||||
return default;
|
||||
}
|
||||
}
|
||||
"""
|
||||
);
|
||||
|
||||
var application = new CliApplicationBuilder()
|
||||
.AddCommand(commandType)
|
||||
.UseConsole(FakeConsole)
|
||||
.Build();
|
||||
|
||||
// Act
|
||||
var exitCode = await application.RunAsync(["-b", "two"], new Dictionary<string, string>());
|
||||
|
||||
// Assert
|
||||
exitCode.Should().Be(0);
|
||||
|
||||
var stdOut = FakeConsole.ReadOutputString();
|
||||
stdOut.Should().ConsistOfLines("Foo = ", "Bar = 2");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task I_can_bind_a_parameter_or_an_option_to_a_string_constructable_object_property()
|
||||
{
|
||||
// Arrange
|
||||
var commandType = DynamicCommandBuilder.Compile(
|
||||
// lang=csharp
|
||||
"""
|
||||
public class CustomType
|
||||
{
|
||||
public string Value { get; }
|
||||
|
||||
public CustomType(string value) => Value = value;
|
||||
}
|
||||
|
||||
[Command]
|
||||
public class Command : ICommand
|
||||
{
|
||||
[CommandOption('f')]
|
||||
public CustomType? Foo { get; init; }
|
||||
|
||||
public ValueTask ExecuteAsync(IConsole console)
|
||||
{
|
||||
console.WriteLine(Foo.Value);
|
||||
return default;
|
||||
}
|
||||
}
|
||||
"""
|
||||
);
|
||||
|
||||
var application = new CliApplicationBuilder()
|
||||
.AddCommand(commandType)
|
||||
.UseConsole(FakeConsole)
|
||||
.Build();
|
||||
|
||||
// Act
|
||||
var exitCode = await application.RunAsync(["-f", "xyz"], new Dictionary<string, string>());
|
||||
|
||||
// Assert
|
||||
exitCode.Should().Be(0);
|
||||
|
||||
var stdOut = FakeConsole.ReadOutputString();
|
||||
stdOut.Trim().Should().Be("xyz");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task I_can_bind_a_parameter_or_an_option_to_a_string_parsable_object_property()
|
||||
{
|
||||
// Arrange
|
||||
var commandType = DynamicCommandBuilder.Compile(
|
||||
// lang=csharp
|
||||
"""
|
||||
public class CustomTypeA
|
||||
{
|
||||
public string Value { get; }
|
||||
|
||||
private CustomTypeA(string value) => Value = value;
|
||||
|
||||
public static CustomTypeA Parse(string value) =>
|
||||
new CustomTypeA(value);
|
||||
}
|
||||
|
||||
public class CustomTypeB
|
||||
{
|
||||
public string Value { get; }
|
||||
|
||||
private CustomTypeB(string value) => Value = value;
|
||||
|
||||
public static CustomTypeB Parse(string value, IFormatProvider formatProvider) =>
|
||||
new CustomTypeB(value);
|
||||
}
|
||||
|
||||
[Command]
|
||||
public class Command : ICommand
|
||||
{
|
||||
[CommandOption('f')]
|
||||
public CustomTypeA? Foo { get; init; }
|
||||
|
||||
[CommandOption('b')]
|
||||
public CustomTypeB? Bar { get; init; }
|
||||
|
||||
public ValueTask ExecuteAsync(IConsole console)
|
||||
{
|
||||
console.WriteLine("Foo = " + Foo.Value);
|
||||
console.WriteLine("Bar = " + Bar.Value);
|
||||
|
||||
return default;
|
||||
}
|
||||
}
|
||||
"""
|
||||
);
|
||||
|
||||
var application = new CliApplicationBuilder()
|
||||
.AddCommand(commandType)
|
||||
.UseConsole(FakeConsole)
|
||||
.Build();
|
||||
|
||||
// Act
|
||||
var exitCode = await application.RunAsync(
|
||||
["-f", "hello", "-b", "world"],
|
||||
new Dictionary<string, string>()
|
||||
);
|
||||
|
||||
// Assert
|
||||
exitCode.Should().Be(0);
|
||||
|
||||
var stdOut = FakeConsole.ReadOutputString();
|
||||
stdOut.Should().ConsistOfLines("Foo = hello", "Bar = world");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task I_can_bind_a_parameter_or_an_option_to_a_property_with_a_custom_converter()
|
||||
{
|
||||
// Arrange
|
||||
var commandType = DynamicCommandBuilder.Compile(
|
||||
// lang=csharp
|
||||
"""
|
||||
public class CustomConverter : BindingConverter<int>
|
||||
{
|
||||
public override int Convert(string rawValue) =>
|
||||
rawValue.Length;
|
||||
}
|
||||
|
||||
[Command]
|
||||
public class Command : ICommand
|
||||
{
|
||||
[CommandOption('f', Converter = typeof(CustomConverter))]
|
||||
public int Foo { get; init; }
|
||||
|
||||
public ValueTask ExecuteAsync(IConsole console)
|
||||
{
|
||||
console.WriteLine(Foo);
|
||||
return default;
|
||||
}
|
||||
}
|
||||
"""
|
||||
);
|
||||
|
||||
var application = new CliApplicationBuilder()
|
||||
.AddCommand(commandType)
|
||||
.UseConsole(FakeConsole)
|
||||
.Build();
|
||||
|
||||
// Act
|
||||
var exitCode = await application.RunAsync(
|
||||
["-f", "hello world"],
|
||||
new Dictionary<string, string>()
|
||||
);
|
||||
|
||||
// Assert
|
||||
exitCode.Should().Be(0);
|
||||
|
||||
var stdOut = FakeConsole.ReadOutputString();
|
||||
stdOut.Trim().Should().Be("11");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task I_can_bind_a_parameter_or_an_option_to_a_string_array_property()
|
||||
{
|
||||
// Arrange
|
||||
var commandType = DynamicCommandBuilder.Compile(
|
||||
// lang=csharp
|
||||
"""
|
||||
[Command]
|
||||
public class Command : ICommand
|
||||
{
|
||||
[CommandOption('f')]
|
||||
public string[]? Foo { get; init; }
|
||||
|
||||
public ValueTask ExecuteAsync(IConsole console)
|
||||
{
|
||||
foreach (var i in Foo)
|
||||
console.WriteLine(i);
|
||||
|
||||
return default;
|
||||
}
|
||||
}
|
||||
"""
|
||||
);
|
||||
|
||||
var application = new CliApplicationBuilder()
|
||||
.AddCommand(commandType)
|
||||
.UseConsole(FakeConsole)
|
||||
.Build();
|
||||
|
||||
// Act
|
||||
var exitCode = await application.RunAsync(
|
||||
["-f", "one", "two", "three"],
|
||||
new Dictionary<string, string>()
|
||||
);
|
||||
|
||||
// Assert
|
||||
exitCode.Should().Be(0);
|
||||
|
||||
var stdOut = FakeConsole.ReadOutputString();
|
||||
stdOut.Should().ConsistOfLines("one", "two", "three");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task I_can_bind_a_parameter_or_an_option_to_a_read_only_list_of_strings_property()
|
||||
{
|
||||
// Arrange
|
||||
var commandType = DynamicCommandBuilder.Compile(
|
||||
// lang=csharp
|
||||
"""
|
||||
[Command]
|
||||
public class Command : ICommand
|
||||
{
|
||||
[CommandOption('f')]
|
||||
public IReadOnlyList<string>? Foo { get; init; }
|
||||
|
||||
public ValueTask ExecuteAsync(IConsole console)
|
||||
{
|
||||
foreach (var i in Foo)
|
||||
console.WriteLine(i);
|
||||
|
||||
return default;
|
||||
}
|
||||
}
|
||||
"""
|
||||
);
|
||||
|
||||
var application = new CliApplicationBuilder()
|
||||
.AddCommand(commandType)
|
||||
.UseConsole(FakeConsole)
|
||||
.Build();
|
||||
|
||||
// Act
|
||||
var exitCode = await application.RunAsync(
|
||||
["-f", "one", "two", "three"],
|
||||
new Dictionary<string, string>()
|
||||
);
|
||||
|
||||
// Assert
|
||||
exitCode.Should().Be(0);
|
||||
|
||||
var stdOut = FakeConsole.ReadOutputString();
|
||||
stdOut.Should().ConsistOfLines("one", "two", "three");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task I_can_bind_a_parameter_or_an_option_to_a_string_list_property()
|
||||
{
|
||||
// Arrange
|
||||
var commandType = DynamicCommandBuilder.Compile(
|
||||
// lang=csharp
|
||||
"""
|
||||
[Command]
|
||||
public class Command : ICommand
|
||||
{
|
||||
[CommandOption('f')]
|
||||
public List<string>? Foo { get; init; }
|
||||
|
||||
public ValueTask ExecuteAsync(IConsole console)
|
||||
{
|
||||
foreach (var i in Foo)
|
||||
console.WriteLine(i);
|
||||
|
||||
return default;
|
||||
}
|
||||
}
|
||||
"""
|
||||
);
|
||||
|
||||
var application = new CliApplicationBuilder()
|
||||
.AddCommand(commandType)
|
||||
.UseConsole(FakeConsole)
|
||||
.Build();
|
||||
|
||||
// Act
|
||||
var exitCode = await application.RunAsync(
|
||||
["-f", "one", "two", "three"],
|
||||
new Dictionary<string, string>()
|
||||
);
|
||||
|
||||
// Assert
|
||||
exitCode.Should().Be(0);
|
||||
|
||||
var stdOut = FakeConsole.ReadOutputString();
|
||||
stdOut.Should().ConsistOfLines("one", "two", "three");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task I_can_bind_a_parameter_or_an_option_to_an_integer_array_property()
|
||||
{
|
||||
// Arrange
|
||||
var commandType = DynamicCommandBuilder.Compile(
|
||||
// lang=csharp
|
||||
"""
|
||||
[Command]
|
||||
public class Command : ICommand
|
||||
{
|
||||
[CommandOption('f')]
|
||||
public int[]? Foo { get; init; }
|
||||
|
||||
public ValueTask ExecuteAsync(IConsole console)
|
||||
{
|
||||
foreach (var i in Foo)
|
||||
console.WriteLine(i);
|
||||
|
||||
return default;
|
||||
}
|
||||
}
|
||||
"""
|
||||
);
|
||||
|
||||
var application = new CliApplicationBuilder()
|
||||
.AddCommand(commandType)
|
||||
.UseConsole(FakeConsole)
|
||||
.Build();
|
||||
|
||||
// Act
|
||||
var exitCode = await application.RunAsync(
|
||||
["-f", "1", "13", "27"],
|
||||
new Dictionary<string, string>()
|
||||
);
|
||||
|
||||
// Assert
|
||||
exitCode.Should().Be(0);
|
||||
|
||||
var stdOut = FakeConsole.ReadOutputString();
|
||||
stdOut.Should().ConsistOfLines("1", "13", "27");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task I_can_try_to_bind_a_parameter_or_an_option_to_a_property_and_get_an_error_if_it_is_of_an_unsupported_type()
|
||||
{
|
||||
// Arrange
|
||||
var commandType = DynamicCommandBuilder.Compile(
|
||||
// lang=csharp
|
||||
"""
|
||||
public class CustomType
|
||||
{
|
||||
}
|
||||
|
||||
[Command]
|
||||
public class Command : ICommand
|
||||
{
|
||||
[CommandOption('f')]
|
||||
public CustomType? Foo { get; init; }
|
||||
|
||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||
}
|
||||
"""
|
||||
);
|
||||
|
||||
var application = new CliApplicationBuilder()
|
||||
.AddCommand(commandType)
|
||||
.UseConsole(FakeConsole)
|
||||
.Build();
|
||||
|
||||
// Act
|
||||
var exitCode = await application.RunAsync(["-f", "xyz"], new Dictionary<string, string>());
|
||||
|
||||
// Assert
|
||||
exitCode.Should().NotBe(0);
|
||||
|
||||
var stdErr = FakeConsole.ReadErrorString();
|
||||
stdErr.Should().Contain("has an unsupported underlying property type");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task I_can_try_to_bind_a_parameter_or_an_option_to_a_non_scalar_property_and_get_an_error_if_it_is_of_an_unsupported_type()
|
||||
{
|
||||
// Arrange
|
||||
var commandType = DynamicCommandBuilder.Compile(
|
||||
// lang=csharp
|
||||
"""
|
||||
public class CustomType : IEnumerable<object>
|
||||
{
|
||||
public IEnumerator<object> GetEnumerator() => Enumerable.Empty<object>().GetEnumerator();
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
|
||||
}
|
||||
|
||||
[Command]
|
||||
public class Command : ICommand
|
||||
{
|
||||
[CommandOption('f')]
|
||||
public CustomType? Foo { get; init; }
|
||||
|
||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||
}
|
||||
"""
|
||||
);
|
||||
|
||||
var application = new CliApplicationBuilder()
|
||||
.AddCommand(commandType)
|
||||
.UseConsole(FakeConsole)
|
||||
.Build();
|
||||
|
||||
// Act
|
||||
var exitCode = await application.RunAsync(
|
||||
["-f", "one", "two"],
|
||||
new Dictionary<string, string>()
|
||||
);
|
||||
|
||||
// Assert
|
||||
exitCode.Should().NotBe(0);
|
||||
|
||||
var stdErr = FakeConsole.ReadErrorString();
|
||||
stdErr.Should().Contain("has an unsupported underlying property type");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task I_can_try_to_bind_a_parameter_or_an_option_to_a_property_and_get_an_error_if_the_user_provides_an_invalid_value()
|
||||
{
|
||||
// Arrange
|
||||
var commandType = DynamicCommandBuilder.Compile(
|
||||
// lang=csharp
|
||||
"""
|
||||
[Command]
|
||||
public class Command : ICommand
|
||||
{
|
||||
[CommandOption('f')]
|
||||
public int Foo { get; init; }
|
||||
|
||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||
}
|
||||
"""
|
||||
);
|
||||
|
||||
var application = new CliApplicationBuilder()
|
||||
.AddCommand(commandType)
|
||||
.UseConsole(FakeConsole)
|
||||
.Build();
|
||||
|
||||
// Act
|
||||
var exitCode = await application.RunAsync(
|
||||
["-f", "12.34"],
|
||||
new Dictionary<string, string>()
|
||||
);
|
||||
|
||||
// Assert
|
||||
exitCode.Should().NotBe(0);
|
||||
|
||||
var stdErr = FakeConsole.ReadErrorString();
|
||||
stdErr.Should().NotBeNullOrWhiteSpace();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task I_can_try_to_bind_a_parameter_or_an_option_to_a_property_and_get_an_error_if_a_custom_validator_fails()
|
||||
{
|
||||
// Arrange
|
||||
var commandType = DynamicCommandBuilder.Compile(
|
||||
// lang=csharp
|
||||
"""
|
||||
public class ValidatorA : BindingValidator<int>
|
||||
{
|
||||
public override BindingValidationError Validate(int value) => Ok();
|
||||
}
|
||||
|
||||
public class ValidatorB : BindingValidator<int>
|
||||
{
|
||||
public override BindingValidationError Validate(int value) => Error("Hello world");
|
||||
}
|
||||
|
||||
[Command]
|
||||
public class Command : ICommand
|
||||
{
|
||||
[CommandOption('f', Validators = [typeof(ValidatorA), typeof(ValidatorB)])]
|
||||
public int Foo { get; init; }
|
||||
|
||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||
}
|
||||
"""
|
||||
);
|
||||
|
||||
var application = new CliApplicationBuilder()
|
||||
.AddCommand(commandType)
|
||||
.UseConsole(FakeConsole)
|
||||
.Build();
|
||||
|
||||
// Act
|
||||
var exitCode = await application.RunAsync(["-f", "12"], new Dictionary<string, string>());
|
||||
|
||||
// Assert
|
||||
exitCode.Should().NotBe(0);
|
||||
|
||||
var stdErr = FakeConsole.ReadErrorString();
|
||||
stdErr.Should().Contain("Hello world");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task I_can_try_to_bind_a_parameter_or_an_option_to_a_string_parsable_property_and_get_an_error_if_the_parsing_fails()
|
||||
{
|
||||
// Arrange
|
||||
var commandType = DynamicCommandBuilder.Compile(
|
||||
// lang=csharp
|
||||
"""
|
||||
public class CustomType
|
||||
{
|
||||
public string Value { get; }
|
||||
|
||||
private CustomType(string value) => Value = value;
|
||||
|
||||
public static CustomType Parse(string value) => throw new Exception("Hello world");
|
||||
}
|
||||
|
||||
[Command]
|
||||
public class Command : ICommand
|
||||
{
|
||||
[CommandOption('f')]
|
||||
public CustomType? Foo { get; init; }
|
||||
|
||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||
}
|
||||
"""
|
||||
);
|
||||
|
||||
var application = new CliApplicationBuilder()
|
||||
.AddCommand(commandType)
|
||||
.UseConsole(FakeConsole)
|
||||
.Build();
|
||||
|
||||
// Act
|
||||
var exitCode = await application.RunAsync(["-f", "bar"], new Dictionary<string, string>());
|
||||
|
||||
// Assert
|
||||
exitCode.Should().NotBe(0);
|
||||
|
||||
var stdErr = FakeConsole.ReadErrorString();
|
||||
stdErr.Should().Contain("Hello world");
|
||||
}
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using CliFx.Exceptions;
|
||||
using CliFx.Tests.TestCommands;
|
||||
using CliFx.Tests.TestCustomTypes;
|
||||
using FluentAssertions;
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace CliFx.Tests
|
||||
{
|
||||
[TestFixture]
|
||||
public class DefaultCommandFactoryTests
|
||||
{
|
||||
private static IEnumerable<TestCaseData> GetTestCases_CreateInstance()
|
||||
{
|
||||
yield return new TestCaseData(typeof(HelloWorldDefaultCommand));
|
||||
}
|
||||
|
||||
private static IEnumerable<TestCaseData> GetTestCases_CreateInstance_Negative()
|
||||
{
|
||||
yield return new TestCaseData(typeof(TestNonStringParseable));
|
||||
}
|
||||
|
||||
[TestCaseSource(nameof(GetTestCases_CreateInstance))]
|
||||
public void CreateInstance_Test(Type type)
|
||||
{
|
||||
// Arrange
|
||||
var activator = new DefaultTypeActivator();
|
||||
|
||||
// Act
|
||||
var obj = activator.CreateInstance(type);
|
||||
|
||||
// Assert
|
||||
obj.Should().BeOfType(type);
|
||||
}
|
||||
|
||||
[TestCaseSource(nameof(GetTestCases_CreateInstance_Negative))]
|
||||
public void CreateInstance_Negative_Test(Type type)
|
||||
{
|
||||
// Arrange
|
||||
var activator = new DefaultTypeActivator();
|
||||
|
||||
// Act & Assert
|
||||
var ex = Assert.Throws<CliFxException>(() => activator.CreateInstance(type));
|
||||
Console.WriteLine(ex.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using CliFx.Exceptions;
|
||||
using CliFx.Tests.TestCommands;
|
||||
using FluentAssertions;
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace CliFx.Tests
|
||||
{
|
||||
[TestFixture]
|
||||
public class DelegateCommandFactoryTests
|
||||
{
|
||||
private static IEnumerable<TestCaseData> GetTestCases_CreateInstance()
|
||||
{
|
||||
yield return new TestCaseData(
|
||||
new Func<Type, object>(Activator.CreateInstance),
|
||||
typeof(HelloWorldDefaultCommand)
|
||||
);
|
||||
}
|
||||
|
||||
private static IEnumerable<TestCaseData> GetTestCases_CreateInstance_Negative()
|
||||
{
|
||||
yield return new TestCaseData(
|
||||
new Func<Type, object>(_ => null),
|
||||
typeof(HelloWorldDefaultCommand)
|
||||
);
|
||||
}
|
||||
|
||||
[TestCaseSource(nameof(GetTestCases_CreateInstance))]
|
||||
public void CreateInstance_Test(Func<Type, object> activatorFunc, Type type)
|
||||
{
|
||||
// Arrange
|
||||
var activator = new DelegateTypeActivator(activatorFunc);
|
||||
|
||||
// Act
|
||||
var obj = activator.CreateInstance(type);
|
||||
|
||||
// Assert
|
||||
obj.Should().BeOfType(type);
|
||||
}
|
||||
|
||||
[TestCaseSource(nameof(GetTestCases_CreateInstance_Negative))]
|
||||
public void CreateInstance_Negative_Test(Func<Type, object> activatorFunc, Type type)
|
||||
{
|
||||
// Arrange
|
||||
var activator = new DelegateTypeActivator(activatorFunc);
|
||||
|
||||
// Act & Assert
|
||||
var ex = Assert.Throws<CliFxException>(() => activator.CreateInstance(type));
|
||||
Console.WriteLine(ex.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
91
CliFx.Tests/DirectivesSpecs.cs
Normal file
91
CliFx.Tests/DirectivesSpecs.cs
Normal file
@@ -0,0 +1,91 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using CliFx.Tests.Utils;
|
||||
using CliFx.Tests.Utils.Extensions;
|
||||
using CliWrap;
|
||||
using FluentAssertions;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace CliFx.Tests;
|
||||
|
||||
public class DirectivesSpecs(ITestOutputHelper testOutput) : SpecsBase(testOutput)
|
||||
{
|
||||
[Fact(Timeout = 15000)]
|
||||
public async Task I_can_use_the_debug_directive_to_make_the_application_wait_for_the_debugger_to_attach()
|
||||
{
|
||||
// Arrange
|
||||
using var cts = new CancellationTokenSource();
|
||||
|
||||
// We can't actually attach a debugger, but we can ensure that the process is waiting for one
|
||||
void HandleStdOut(string line)
|
||||
{
|
||||
// Kill the process once it writes the output we expect
|
||||
if (line.Contains("Attach the debugger to", StringComparison.OrdinalIgnoreCase))
|
||||
cts.Cancel();
|
||||
}
|
||||
|
||||
var command = Cli.Wrap(Dummy.Program.FilePath).WithArguments("[debug]") | HandleStdOut;
|
||||
|
||||
// Act & assert
|
||||
try
|
||||
{
|
||||
await command.ExecuteAsync(cts.Token);
|
||||
}
|
||||
catch (OperationCanceledException ex) when (ex.CancellationToken == cts.Token)
|
||||
{
|
||||
// This means that the process was killed after it wrote the expected output
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task I_can_use_the_preview_directive_to_make_the_application_print_the_parsed_command_input()
|
||||
{
|
||||
// Arrange
|
||||
var commandType = DynamicCommandBuilder.Compile(
|
||||
// lang=csharp
|
||||
"""
|
||||
[Command("cmd")]
|
||||
public class Command : ICommand
|
||||
{
|
||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||
}
|
||||
"""
|
||||
);
|
||||
|
||||
var application = new CliApplicationBuilder()
|
||||
.AddCommand(commandType)
|
||||
.UseConsole(FakeConsole)
|
||||
.AllowPreviewMode()
|
||||
.Build();
|
||||
|
||||
// Act
|
||||
var exitCode = await application.RunAsync(
|
||||
["[preview]", "cmd", "param", "-abc", "--option", "foo"],
|
||||
new Dictionary<string, string> { ["ENV_QOP"] = "hello", ["ENV_KIL"] = "world" }
|
||||
);
|
||||
|
||||
// Assert
|
||||
exitCode.Should().Be(0);
|
||||
|
||||
var stdOut = FakeConsole.ReadOutputString();
|
||||
stdOut
|
||||
.Should()
|
||||
.ContainAllInOrder(
|
||||
"cmd",
|
||||
"<param>",
|
||||
"[-a]",
|
||||
"[-b]",
|
||||
"[-c]",
|
||||
"[--option \"foo\"]",
|
||||
"ENV_QOP",
|
||||
"=",
|
||||
"\"hello\"",
|
||||
"ENV_KIL",
|
||||
"=",
|
||||
"\"world\""
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,888 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using CliFx.Domain;
|
||||
using CliFx.Exceptions;
|
||||
using CliFx.Tests.TestCommands;
|
||||
using CliFx.Tests.TestCustomTypes;
|
||||
using FluentAssertions;
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace CliFx.Tests.Domain
|
||||
{
|
||||
[TestFixture]
|
||||
internal partial class ApplicationSchemaTests
|
||||
{
|
||||
private static IEnumerable<TestCaseData> GetTestCases_Resolve()
|
||||
{
|
||||
yield return new TestCaseData(
|
||||
new[]
|
||||
{
|
||||
typeof(DivideCommand),
|
||||
typeof(ConcatCommand),
|
||||
typeof(EnvironmentVariableCommand)
|
||||
},
|
||||
new[]
|
||||
{
|
||||
new CommandSchema(typeof(DivideCommand), "div", "Divide one number by another.",
|
||||
new CommandParameterSchema[0], new[]
|
||||
{
|
||||
new CommandOptionSchema(typeof(DivideCommand).GetProperty(nameof(DivideCommand.Dividend)),
|
||||
"dividend", 'D', null, true, "The number to divide."),
|
||||
new CommandOptionSchema(typeof(DivideCommand).GetProperty(nameof(DivideCommand.Divisor)),
|
||||
"divisor", 'd', null, true, "The number to divide by.")
|
||||
}),
|
||||
new CommandSchema(typeof(ConcatCommand), "concat", "Concatenate strings.",
|
||||
new CommandParameterSchema[0],
|
||||
new[]
|
||||
{
|
||||
new CommandOptionSchema(typeof(ConcatCommand).GetProperty(nameof(ConcatCommand.Inputs)),
|
||||
null, 'i', null, true, "Input strings."),
|
||||
new CommandOptionSchema(typeof(ConcatCommand).GetProperty(nameof(ConcatCommand.Separator)),
|
||||
null, 's', null, false, "String separator.")
|
||||
}),
|
||||
new CommandSchema(typeof(EnvironmentVariableCommand), null, "Reads option values from environment variables.",
|
||||
new CommandParameterSchema[0],
|
||||
new[]
|
||||
{
|
||||
new CommandOptionSchema(typeof(EnvironmentVariableCommand).GetProperty(nameof(EnvironmentVariableCommand.Option)),
|
||||
"opt", null, "ENV_SINGLE_VALUE", false, null)
|
||||
}
|
||||
)
|
||||
}
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {typeof(SimpleParameterCommand)},
|
||||
new[]
|
||||
{
|
||||
new CommandSchema(typeof(SimpleParameterCommand), "param cmd2", "Command using positional parameters",
|
||||
new[]
|
||||
{
|
||||
new CommandParameterSchema(typeof(SimpleParameterCommand).GetProperty(nameof(SimpleParameterCommand.ParameterA)),
|
||||
0, "first", null),
|
||||
new CommandParameterSchema(typeof(SimpleParameterCommand).GetProperty(nameof(SimpleParameterCommand.ParameterB)),
|
||||
10, null, null)
|
||||
},
|
||||
new[]
|
||||
{
|
||||
new CommandOptionSchema(typeof(SimpleParameterCommand).GetProperty(nameof(SimpleParameterCommand.OptionA)),
|
||||
"option", 'o', null, false, null)
|
||||
})
|
||||
}
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {typeof(HelloWorldDefaultCommand)},
|
||||
new[]
|
||||
{
|
||||
new CommandSchema(typeof(HelloWorldDefaultCommand), null, null,
|
||||
new CommandParameterSchema[0],
|
||||
new CommandOptionSchema[0])
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private static IEnumerable<TestCaseData> GetTestCases_Resolve_Negative()
|
||||
{
|
||||
yield return new TestCaseData(new object[]
|
||||
{
|
||||
new Type[0]
|
||||
});
|
||||
|
||||
// Command validation failure
|
||||
|
||||
yield return new TestCaseData(new object[]
|
||||
{
|
||||
new[] {typeof(NonImplementedCommand)}
|
||||
});
|
||||
|
||||
yield return new TestCaseData(new object[]
|
||||
{
|
||||
// Same name
|
||||
new[] {typeof(ExceptionCommand), typeof(CommandExceptionCommand)}
|
||||
});
|
||||
|
||||
yield return new TestCaseData(new object[]
|
||||
{
|
||||
new[] {typeof(NonAnnotatedCommand)}
|
||||
});
|
||||
|
||||
// Parameter validation failure
|
||||
|
||||
yield return new TestCaseData(new object[]
|
||||
{
|
||||
new[] {typeof(DuplicateParameterOrderCommand)}
|
||||
});
|
||||
|
||||
yield return new TestCaseData(new object[]
|
||||
{
|
||||
new[] {typeof(DuplicateParameterNameCommand)}
|
||||
});
|
||||
|
||||
yield return new TestCaseData(new object[]
|
||||
{
|
||||
new[] {typeof(MultipleNonScalarParametersCommand)}
|
||||
});
|
||||
|
||||
yield return new TestCaseData(new object[]
|
||||
{
|
||||
new[] {typeof(NonLastNonScalarParameterCommand)}
|
||||
});
|
||||
|
||||
// Option validation failure
|
||||
|
||||
yield return new TestCaseData(new object[]
|
||||
{
|
||||
new[] {typeof(DuplicateOptionNamesCommand)}
|
||||
});
|
||||
|
||||
yield return new TestCaseData(new object[]
|
||||
{
|
||||
new[] {typeof(DuplicateOptionShortNamesCommand)}
|
||||
});
|
||||
|
||||
yield return new TestCaseData(new object[]
|
||||
{
|
||||
new[] {typeof(DuplicateOptionEnvironmentVariableNamesCommand)}
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
[TestCaseSource(nameof(GetTestCases_Resolve))]
|
||||
public void Resolve_Test(
|
||||
IReadOnlyList<Type> commandTypes,
|
||||
IReadOnlyList<CommandSchema> expectedCommandSchemas)
|
||||
{
|
||||
// Act
|
||||
var applicationSchema = ApplicationSchema.Resolve(commandTypes);
|
||||
|
||||
// Assert
|
||||
applicationSchema.Commands.Should().BeEquivalentTo(expectedCommandSchemas);
|
||||
}
|
||||
|
||||
[Test]
|
||||
[TestCaseSource(nameof(GetTestCases_Resolve_Negative))]
|
||||
public void Resolve_Negative_Test(IReadOnlyList<Type> commandTypes)
|
||||
{
|
||||
// Act & Assert
|
||||
var ex = Assert.Throws<CliFxException>(() => ApplicationSchema.Resolve(commandTypes));
|
||||
Console.WriteLine(ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
internal partial class ApplicationSchemaTests
|
||||
{
|
||||
private static IEnumerable<TestCaseData> GetTestCases_InitializeEntryPoint()
|
||||
{
|
||||
yield return new TestCaseData(
|
||||
new[] {typeof(AllSupportedTypesCommand)},
|
||||
new CommandLineInput(
|
||||
new[]
|
||||
{
|
||||
new CommandOptionInput(nameof(AllSupportedTypesCommand.Object), "value")
|
||||
}),
|
||||
new Dictionary<string, string>(),
|
||||
new AllSupportedTypesCommand {Object = "value"}
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {typeof(AllSupportedTypesCommand)},
|
||||
new CommandLineInput(
|
||||
new[]
|
||||
{
|
||||
new CommandOptionInput(nameof(AllSupportedTypesCommand.String), "value")
|
||||
}),
|
||||
new Dictionary<string, string>(),
|
||||
new AllSupportedTypesCommand {String = "value"}
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {typeof(AllSupportedTypesCommand)},
|
||||
new CommandLineInput(
|
||||
new[]
|
||||
{
|
||||
new CommandOptionInput(nameof(AllSupportedTypesCommand.Bool), "true")
|
||||
}),
|
||||
new Dictionary<string, string>(),
|
||||
new AllSupportedTypesCommand {Bool = true}
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {typeof(AllSupportedTypesCommand)},
|
||||
new CommandLineInput(
|
||||
new[]
|
||||
{
|
||||
new CommandOptionInput(nameof(AllSupportedTypesCommand.Bool), "false")
|
||||
}),
|
||||
new Dictionary<string, string>(),
|
||||
new AllSupportedTypesCommand {Bool = false}
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {typeof(AllSupportedTypesCommand)},
|
||||
new CommandLineInput(
|
||||
new[]
|
||||
{
|
||||
new CommandOptionInput(nameof(AllSupportedTypesCommand.Bool))
|
||||
}),
|
||||
new Dictionary<string, string>(),
|
||||
new AllSupportedTypesCommand {Bool = true}
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {typeof(AllSupportedTypesCommand)},
|
||||
new CommandLineInput(
|
||||
new[]
|
||||
{
|
||||
new CommandOptionInput(nameof(AllSupportedTypesCommand.Char), "a")
|
||||
}),
|
||||
new Dictionary<string, string>(),
|
||||
new AllSupportedTypesCommand {Char = 'a'}
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {typeof(AllSupportedTypesCommand)},
|
||||
new CommandLineInput(
|
||||
new[]
|
||||
{
|
||||
new CommandOptionInput(nameof(AllSupportedTypesCommand.Sbyte), "15")
|
||||
}),
|
||||
new Dictionary<string, string>(),
|
||||
new AllSupportedTypesCommand {Sbyte = 15}
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {typeof(AllSupportedTypesCommand)},
|
||||
new CommandLineInput(
|
||||
new[]
|
||||
{
|
||||
new CommandOptionInput(nameof(AllSupportedTypesCommand.Byte), "15")
|
||||
}),
|
||||
new Dictionary<string, string>(),
|
||||
new AllSupportedTypesCommand {Byte = 15}
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {typeof(AllSupportedTypesCommand)},
|
||||
new CommandLineInput(
|
||||
new[]
|
||||
{
|
||||
new CommandOptionInput(nameof(AllSupportedTypesCommand.Short), "15")
|
||||
}),
|
||||
new Dictionary<string, string>(),
|
||||
new AllSupportedTypesCommand {Short = 15}
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {typeof(AllSupportedTypesCommand)},
|
||||
new CommandLineInput(
|
||||
new[]
|
||||
{
|
||||
new CommandOptionInput(nameof(AllSupportedTypesCommand.Ushort), "15")
|
||||
}),
|
||||
new Dictionary<string, string>(),
|
||||
new AllSupportedTypesCommand {Ushort = 15}
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {typeof(AllSupportedTypesCommand)},
|
||||
new CommandLineInput(
|
||||
new[]
|
||||
{
|
||||
new CommandOptionInput(nameof(AllSupportedTypesCommand.Int), "15")
|
||||
}),
|
||||
new Dictionary<string, string>(),
|
||||
new AllSupportedTypesCommand {Int = 15}
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {typeof(AllSupportedTypesCommand)},
|
||||
new CommandLineInput(
|
||||
new[]
|
||||
{
|
||||
new CommandOptionInput(nameof(AllSupportedTypesCommand.Uint), "15")
|
||||
}),
|
||||
new Dictionary<string, string>(),
|
||||
new AllSupportedTypesCommand {Uint = 15}
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {typeof(AllSupportedTypesCommand)},
|
||||
new CommandLineInput(
|
||||
new[]
|
||||
{
|
||||
new CommandOptionInput(nameof(AllSupportedTypesCommand.Long), "15")
|
||||
}),
|
||||
new Dictionary<string, string>(),
|
||||
new AllSupportedTypesCommand {Long = 15}
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {typeof(AllSupportedTypesCommand)},
|
||||
new CommandLineInput(
|
||||
new[]
|
||||
{
|
||||
new CommandOptionInput(nameof(AllSupportedTypesCommand.Ulong), "15")
|
||||
}),
|
||||
new Dictionary<string, string>(),
|
||||
new AllSupportedTypesCommand {Ulong = 15}
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {typeof(AllSupportedTypesCommand)},
|
||||
new CommandLineInput(
|
||||
new[]
|
||||
{
|
||||
new CommandOptionInput(nameof(AllSupportedTypesCommand.Float), "123.45")
|
||||
}),
|
||||
new Dictionary<string, string>(),
|
||||
new AllSupportedTypesCommand {Float = 123.45f}
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {typeof(AllSupportedTypesCommand)},
|
||||
new CommandLineInput(
|
||||
new[]
|
||||
{
|
||||
new CommandOptionInput(nameof(AllSupportedTypesCommand.Double), "123.45")
|
||||
}),
|
||||
new Dictionary<string, string>(),
|
||||
new AllSupportedTypesCommand {Double = 123.45}
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {typeof(AllSupportedTypesCommand)},
|
||||
new CommandLineInput(
|
||||
new[]
|
||||
{
|
||||
new CommandOptionInput(nameof(AllSupportedTypesCommand.Decimal), "123.45")
|
||||
}),
|
||||
new Dictionary<string, string>(),
|
||||
new AllSupportedTypesCommand {Decimal = 123.45m}
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {typeof(AllSupportedTypesCommand)},
|
||||
new CommandLineInput(
|
||||
new[]
|
||||
{
|
||||
new CommandOptionInput(nameof(AllSupportedTypesCommand.DateTime), "28 Apr 1995")
|
||||
}),
|
||||
new Dictionary<string, string>(),
|
||||
new AllSupportedTypesCommand {DateTime = new DateTime(1995, 04, 28)}
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {typeof(AllSupportedTypesCommand)},
|
||||
new CommandLineInput(
|
||||
new[]
|
||||
{
|
||||
new CommandOptionInput(nameof(AllSupportedTypesCommand.DateTimeOffset), "28 Apr 1995")
|
||||
}),
|
||||
new Dictionary<string, string>(),
|
||||
new AllSupportedTypesCommand {DateTimeOffset = new DateTime(1995, 04, 28)}
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {typeof(AllSupportedTypesCommand)},
|
||||
new CommandLineInput(
|
||||
new[]
|
||||
{
|
||||
new CommandOptionInput(nameof(AllSupportedTypesCommand.TimeSpan), "00:14:59")
|
||||
}),
|
||||
new Dictionary<string, string>(),
|
||||
new AllSupportedTypesCommand {TimeSpan = new TimeSpan(00, 14, 59)}
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {typeof(AllSupportedTypesCommand)},
|
||||
new CommandLineInput(
|
||||
new[]
|
||||
{
|
||||
new CommandOptionInput(nameof(AllSupportedTypesCommand.TestEnum), "value2")
|
||||
}),
|
||||
new Dictionary<string, string>(),
|
||||
new AllSupportedTypesCommand {TestEnum = TestEnum.Value2}
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {typeof(AllSupportedTypesCommand)},
|
||||
new CommandLineInput(
|
||||
new[]
|
||||
{
|
||||
new CommandOptionInput(nameof(AllSupportedTypesCommand.IntNullable), "666")
|
||||
}),
|
||||
new Dictionary<string, string>(),
|
||||
new AllSupportedTypesCommand {IntNullable = 666}
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {typeof(AllSupportedTypesCommand)},
|
||||
new CommandLineInput(
|
||||
new[]
|
||||
{
|
||||
new CommandOptionInput(nameof(AllSupportedTypesCommand.IntNullable))
|
||||
}),
|
||||
new Dictionary<string, string>(),
|
||||
new AllSupportedTypesCommand {IntNullable = null}
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {typeof(AllSupportedTypesCommand)},
|
||||
new CommandLineInput(
|
||||
new[]
|
||||
{
|
||||
new CommandOptionInput(nameof(AllSupportedTypesCommand.TestEnumNullable), "value3")
|
||||
}),
|
||||
new Dictionary<string, string>(),
|
||||
new AllSupportedTypesCommand {TestEnumNullable = TestEnum.Value3}
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {typeof(AllSupportedTypesCommand)},
|
||||
new CommandLineInput(
|
||||
new[]
|
||||
{
|
||||
new CommandOptionInput(nameof(AllSupportedTypesCommand.TestEnumNullable))
|
||||
}),
|
||||
new Dictionary<string, string>(),
|
||||
new AllSupportedTypesCommand {TestEnumNullable = null}
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {typeof(AllSupportedTypesCommand)},
|
||||
new CommandLineInput(
|
||||
new[]
|
||||
{
|
||||
new CommandOptionInput(nameof(AllSupportedTypesCommand.TimeSpanNullable), "01:00:00")
|
||||
}),
|
||||
new Dictionary<string, string>(),
|
||||
new AllSupportedTypesCommand {TimeSpanNullable = new TimeSpan(01, 00, 00)}
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {typeof(AllSupportedTypesCommand)},
|
||||
new CommandLineInput(
|
||||
new[]
|
||||
{
|
||||
new CommandOptionInput(nameof(AllSupportedTypesCommand.TimeSpanNullable))
|
||||
}),
|
||||
new Dictionary<string, string>(),
|
||||
new AllSupportedTypesCommand {TimeSpanNullable = null}
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {typeof(AllSupportedTypesCommand)},
|
||||
new CommandLineInput(
|
||||
new[]
|
||||
{
|
||||
new CommandOptionInput(nameof(AllSupportedTypesCommand.TestStringConstructable), "value")
|
||||
}),
|
||||
new Dictionary<string, string>(),
|
||||
new AllSupportedTypesCommand {TestStringConstructable = new TestStringConstructable("value")}
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {typeof(AllSupportedTypesCommand)},
|
||||
new CommandLineInput(
|
||||
new[]
|
||||
{
|
||||
new CommandOptionInput(nameof(AllSupportedTypesCommand.TestStringParseable), "value")
|
||||
}),
|
||||
new Dictionary<string, string>(),
|
||||
new AllSupportedTypesCommand {TestStringParseable = TestStringParseable.Parse("value")}
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {typeof(AllSupportedTypesCommand)},
|
||||
new CommandLineInput(
|
||||
new[]
|
||||
{
|
||||
new CommandOptionInput(nameof(AllSupportedTypesCommand.TestStringParseableWithFormatProvider), "value")
|
||||
}),
|
||||
new Dictionary<string, string>(),
|
||||
new AllSupportedTypesCommand
|
||||
{
|
||||
TestStringParseableWithFormatProvider =
|
||||
TestStringParseableWithFormatProvider.Parse("value", CultureInfo.InvariantCulture)
|
||||
}
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {typeof(AllSupportedTypesCommand)},
|
||||
new CommandLineInput(
|
||||
new[]
|
||||
{
|
||||
new CommandOptionInput(nameof(AllSupportedTypesCommand.ObjectArray), new[] {"value1", "value2"})
|
||||
}),
|
||||
new Dictionary<string, string>(),
|
||||
new AllSupportedTypesCommand {ObjectArray = new object[] {"value1", "value2"}}
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {typeof(AllSupportedTypesCommand)},
|
||||
new CommandLineInput(
|
||||
new[]
|
||||
{
|
||||
new CommandOptionInput(nameof(AllSupportedTypesCommand.StringArray), new[] {"value1", "value2"})
|
||||
}),
|
||||
new Dictionary<string, string>(),
|
||||
new AllSupportedTypesCommand {StringArray = new[] {"value1", "value2"}}
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {typeof(AllSupportedTypesCommand)},
|
||||
new CommandLineInput(
|
||||
new[]
|
||||
{
|
||||
new CommandOptionInput(nameof(AllSupportedTypesCommand.StringArray))
|
||||
}),
|
||||
new Dictionary<string, string>(),
|
||||
new AllSupportedTypesCommand {StringArray = new string[0]}
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {typeof(AllSupportedTypesCommand)},
|
||||
new CommandLineInput(
|
||||
new[]
|
||||
{
|
||||
new CommandOptionInput(nameof(AllSupportedTypesCommand.IntArray), new[] {"47", "69"})
|
||||
}),
|
||||
new Dictionary<string, string>(),
|
||||
new AllSupportedTypesCommand {IntArray = new[] {47, 69}}
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {typeof(AllSupportedTypesCommand)},
|
||||
new CommandLineInput(
|
||||
new[]
|
||||
{
|
||||
new CommandOptionInput(nameof(AllSupportedTypesCommand.TestEnumArray), new[] {"value1", "value3"})
|
||||
}),
|
||||
new Dictionary<string, string>(),
|
||||
new AllSupportedTypesCommand {TestEnumArray = new[] {TestEnum.Value1, TestEnum.Value3}}
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {typeof(AllSupportedTypesCommand)},
|
||||
new CommandLineInput(
|
||||
new[]
|
||||
{
|
||||
new CommandOptionInput(nameof(AllSupportedTypesCommand.IntNullableArray), new[] {"1337", "2441"})
|
||||
}),
|
||||
new Dictionary<string, string>(),
|
||||
new AllSupportedTypesCommand {IntNullableArray = new int?[] {1337, 2441}}
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {typeof(AllSupportedTypesCommand)},
|
||||
new CommandLineInput(
|
||||
new[]
|
||||
{
|
||||
new CommandOptionInput(nameof(AllSupportedTypesCommand.TestStringConstructableArray), new[] {"value1", "value2"})
|
||||
}),
|
||||
new Dictionary<string, string>(),
|
||||
new AllSupportedTypesCommand
|
||||
{
|
||||
TestStringConstructableArray = new[]
|
||||
{
|
||||
new TestStringConstructable("value1"),
|
||||
new TestStringConstructable("value2")
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {typeof(AllSupportedTypesCommand)},
|
||||
new CommandLineInput(
|
||||
new[]
|
||||
{
|
||||
new CommandOptionInput(nameof(AllSupportedTypesCommand.Enumerable), new[] {"value1", "value3"})
|
||||
}),
|
||||
new Dictionary<string, string>(),
|
||||
new AllSupportedTypesCommand {Enumerable = new[] {"value1", "value3"}}
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {typeof(AllSupportedTypesCommand)},
|
||||
new CommandLineInput(
|
||||
new[]
|
||||
{
|
||||
new CommandOptionInput(nameof(AllSupportedTypesCommand.StringEnumerable), new[] {"value1", "value3"})
|
||||
}),
|
||||
new Dictionary<string, string>(),
|
||||
new AllSupportedTypesCommand {StringEnumerable = new[] {"value1", "value3"}}
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {typeof(AllSupportedTypesCommand)},
|
||||
new CommandLineInput(
|
||||
new[]
|
||||
{
|
||||
new CommandOptionInput(nameof(AllSupportedTypesCommand.StringReadOnlyList), new[] {"value1", "value3"})
|
||||
}),
|
||||
new Dictionary<string, string>(),
|
||||
new AllSupportedTypesCommand {StringReadOnlyList = new[] {"value1", "value3"}}
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {typeof(AllSupportedTypesCommand)},
|
||||
new CommandLineInput(
|
||||
new[]
|
||||
{
|
||||
new CommandOptionInput(nameof(AllSupportedTypesCommand.StringList), new[] {"value1", "value3"})
|
||||
}),
|
||||
new Dictionary<string, string>(),
|
||||
new AllSupportedTypesCommand {StringList = new List<string> {"value1", "value3"}}
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {typeof(AllSupportedTypesCommand)},
|
||||
new CommandLineInput(
|
||||
new[]
|
||||
{
|
||||
new CommandOptionInput(nameof(AllSupportedTypesCommand.StringHashSet), new[] {"value1", "value3"})
|
||||
}),
|
||||
new Dictionary<string, string>(),
|
||||
new AllSupportedTypesCommand {StringHashSet = new HashSet<string> {"value1", "value3"}}
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {typeof(DivideCommand)},
|
||||
new CommandLineInput(
|
||||
new[] {"div"},
|
||||
new[]
|
||||
{
|
||||
new CommandOptionInput("dividend", "13"),
|
||||
new CommandOptionInput("divisor", "8"),
|
||||
}),
|
||||
new Dictionary<string, string>(),
|
||||
new DivideCommand {Dividend = 13, Divisor = 8}
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {typeof(DivideCommand)},
|
||||
new CommandLineInput(
|
||||
new[] {"div"},
|
||||
new[]
|
||||
{
|
||||
new CommandOptionInput("D", "13"),
|
||||
new CommandOptionInput("d", "8"),
|
||||
}),
|
||||
new Dictionary<string, string>(),
|
||||
new DivideCommand {Dividend = 13, Divisor = 8}
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {typeof(DivideCommand)},
|
||||
new CommandLineInput(
|
||||
new[] {"div"},
|
||||
new[]
|
||||
{
|
||||
new CommandOptionInput("dividend", "13"),
|
||||
new CommandOptionInput("d", "8"),
|
||||
}),
|
||||
new Dictionary<string, string>(),
|
||||
new DivideCommand {Dividend = 13, Divisor = 8}
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {typeof(ConcatCommand)},
|
||||
new CommandLineInput(
|
||||
new[] {"concat"},
|
||||
new[] {new CommandOptionInput("i", new[] {"foo", " ", "bar"}),}),
|
||||
new Dictionary<string, string>(),
|
||||
new ConcatCommand {Inputs = new[] {"foo", " ", "bar"}}
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {typeof(ConcatCommand)},
|
||||
new CommandLineInput(
|
||||
new[] {"concat"},
|
||||
new[]
|
||||
{
|
||||
new CommandOptionInput("i", new[] {"foo", "bar"}),
|
||||
new CommandOptionInput("s", " "),
|
||||
}),
|
||||
new Dictionary<string, string>(),
|
||||
new ConcatCommand {Inputs = new[] {"foo", "bar"}, Separator = " "}
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {typeof(EnvironmentVariableCommand)},
|
||||
CommandLineInput.Empty,
|
||||
new Dictionary<string, string>
|
||||
{
|
||||
["ENV_SINGLE_VALUE"] = "A"
|
||||
},
|
||||
new EnvironmentVariableCommand {Option = "A"}
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {typeof(EnvironmentVariableWithMultipleValuesCommand)},
|
||||
CommandLineInput.Empty,
|
||||
new Dictionary<string, string>
|
||||
{
|
||||
["ENV_MULTIPLE_VALUES"] = string.Join(Path.PathSeparator, "A", "B", "C")
|
||||
},
|
||||
new EnvironmentVariableWithMultipleValuesCommand {Option = new[] {"A", "B", "C"}}
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {typeof(EnvironmentVariableCommand)},
|
||||
new CommandLineInput(new[] {new CommandOptionInput("opt", "X")}),
|
||||
new Dictionary<string, string>
|
||||
{
|
||||
["ENV_SINGLE_VALUE"] = "A"
|
||||
},
|
||||
new EnvironmentVariableCommand {Option = "X"}
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {typeof(EnvironmentVariableWithoutCollectionPropertyCommand)},
|
||||
CommandLineInput.Empty,
|
||||
new Dictionary<string, string>
|
||||
{
|
||||
["ENV_MULTIPLE_VALUES"] = string.Join(Path.PathSeparator, "A", "B", "C")
|
||||
},
|
||||
new EnvironmentVariableWithoutCollectionPropertyCommand {Option = string.Join(Path.PathSeparator, "A", "B", "C")}
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {typeof(ParameterCommand)},
|
||||
new CommandLineInput(
|
||||
new[] {"param", "cmd", "abc", "123", "1", "2"},
|
||||
new[] {new CommandOptionInput("o", "option value")}),
|
||||
new Dictionary<string, string>(),
|
||||
new ParameterCommand
|
||||
{
|
||||
ParameterA = "abc",
|
||||
ParameterB = 123,
|
||||
ParameterC = new[] {1, 2},
|
||||
OptionA = "option value"
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private static IEnumerable<TestCaseData> GetTestCases_InitializeEntryPoint_Negative()
|
||||
{
|
||||
yield return new TestCaseData(
|
||||
new[] {typeof(AllSupportedTypesCommand)},
|
||||
new CommandLineInput(
|
||||
new[] {new CommandOptionInput(nameof(AllSupportedTypesCommand.Int), "1234.5")}),
|
||||
new Dictionary<string, string>()
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {typeof(AllSupportedTypesCommand)},
|
||||
new CommandLineInput(
|
||||
new[] {new CommandOptionInput(nameof(AllSupportedTypesCommand.Int), new[] {"123", "456"})}),
|
||||
new Dictionary<string, string>()
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {typeof(AllSupportedTypesCommand)},
|
||||
new CommandLineInput(
|
||||
new[] {new CommandOptionInput(nameof(AllSupportedTypesCommand.Int))}),
|
||||
new Dictionary<string, string>()
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {typeof(AllSupportedTypesCommand)},
|
||||
new CommandLineInput(
|
||||
new[] {new CommandOptionInput(nameof(AllSupportedTypesCommand.NonConvertible), "123")}),
|
||||
new Dictionary<string, string>()
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {typeof(DivideCommand)},
|
||||
new CommandLineInput(new[] {"div"}),
|
||||
new Dictionary<string, string>()
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {typeof(DivideCommand)},
|
||||
new CommandLineInput(new[] {"div", "-D", "13"}),
|
||||
new Dictionary<string, string>()
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {typeof(ConcatCommand)},
|
||||
new CommandLineInput(new[] {"concat"}),
|
||||
new Dictionary<string, string>()
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {typeof(ConcatCommand)},
|
||||
new CommandLineInput(
|
||||
new[] {"concat"},
|
||||
new[] {new CommandOptionInput("s", "_")}),
|
||||
new Dictionary<string, string>()
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {typeof(ParameterCommand)},
|
||||
new CommandLineInput(
|
||||
new[] {"param", "cmd"},
|
||||
new[] {new CommandOptionInput("o", "option value")}),
|
||||
new Dictionary<string, string>()
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {typeof(ParameterCommand)},
|
||||
new CommandLineInput(
|
||||
new[] {"param", "cmd", "abc", "123", "invalid"},
|
||||
new[] {new CommandOptionInput("o", "option value")}),
|
||||
new Dictionary<string, string>()
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {typeof(DivideCommand)},
|
||||
new CommandLineInput(new[] {"non-existing"}),
|
||||
new Dictionary<string, string>()
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {typeof(BrokenEnumerableCommand)},
|
||||
new CommandLineInput(new[] {"value1", "value2"}),
|
||||
new Dictionary<string, string>()
|
||||
);
|
||||
}
|
||||
|
||||
[TestCaseSource(nameof(GetTestCases_InitializeEntryPoint))]
|
||||
public void InitializeEntryPoint_Test(
|
||||
IReadOnlyList<Type> commandTypes,
|
||||
CommandLineInput commandLineInput,
|
||||
IReadOnlyDictionary<string, string> environmentVariables,
|
||||
ICommand expectedResult)
|
||||
{
|
||||
// Arrange
|
||||
var applicationSchema = ApplicationSchema.Resolve(commandTypes);
|
||||
var typeActivator = new DefaultTypeActivator();
|
||||
|
||||
// Act
|
||||
var command = applicationSchema.InitializeEntryPoint(commandLineInput, environmentVariables, typeActivator);
|
||||
|
||||
// Assert
|
||||
command.Should().BeEquivalentTo(expectedResult, o => o.RespectingRuntimeTypes());
|
||||
}
|
||||
|
||||
[TestCaseSource(nameof(GetTestCases_InitializeEntryPoint_Negative))]
|
||||
public void InitializeEntryPoint_Negative_Test(
|
||||
IReadOnlyList<Type> commandTypes,
|
||||
CommandLineInput commandLineInput,
|
||||
IReadOnlyDictionary<string, string> environmentVariables)
|
||||
{
|
||||
// Arrange
|
||||
var applicationSchema = ApplicationSchema.Resolve(commandTypes);
|
||||
var typeActivator = new DefaultTypeActivator();
|
||||
|
||||
// Act & Assert
|
||||
var ex = Assert.Throws<CliFxException>(() =>
|
||||
applicationSchema.InitializeEntryPoint(commandLineInput, environmentVariables, typeActivator));
|
||||
Console.WriteLine(ex.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,264 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using CliFx.Domain;
|
||||
using FluentAssertions;
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace CliFx.Tests.Domain
|
||||
{
|
||||
[TestFixture]
|
||||
internal class CommandLineInputTests
|
||||
{
|
||||
private static IEnumerable<TestCaseData> GetTestCases_Parse()
|
||||
{
|
||||
yield return new TestCaseData(
|
||||
new string[0],
|
||||
CommandLineInput.Empty
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {"param"},
|
||||
new CommandLineInput(
|
||||
new[] {"param"})
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {"cmd", "param"},
|
||||
new CommandLineInput(
|
||||
new[] {"cmd", "param"})
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {"--option", "value"},
|
||||
new CommandLineInput(
|
||||
new[]
|
||||
{
|
||||
new CommandOptionInput("option", "value")
|
||||
})
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {"--option1", "value1", "--option2", "value2"},
|
||||
new CommandLineInput(
|
||||
new[]
|
||||
{
|
||||
new CommandOptionInput("option1", "value1"),
|
||||
new CommandOptionInput("option2", "value2")
|
||||
})
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {"--option", "value1", "value2"},
|
||||
new CommandLineInput(
|
||||
new[]
|
||||
{
|
||||
new CommandOptionInput("option", new[] {"value1", "value2"})
|
||||
})
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {"--option", "value1", "--option", "value2"},
|
||||
new CommandLineInput(
|
||||
new[]
|
||||
{
|
||||
new CommandOptionInput("option", new[] {"value1", "value2"})
|
||||
})
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {"-a", "value"},
|
||||
new CommandLineInput(
|
||||
new[]
|
||||
{
|
||||
new CommandOptionInput("a", "value")
|
||||
})
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {"-a", "value1", "-b", "value2"},
|
||||
new CommandLineInput(
|
||||
new[]
|
||||
{
|
||||
new CommandOptionInput("a", "value1"),
|
||||
new CommandOptionInput("b", "value2")
|
||||
})
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {"-a", "value1", "value2"},
|
||||
new CommandLineInput(
|
||||
new[]
|
||||
{
|
||||
new CommandOptionInput("a", new[] {"value1", "value2"})
|
||||
})
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {"-a", "value1", "-a", "value2"},
|
||||
new CommandLineInput(
|
||||
new[]
|
||||
{
|
||||
new CommandOptionInput("a", new[] {"value1", "value2"})
|
||||
})
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {"--option1", "value1", "-b", "value2"},
|
||||
new CommandLineInput(
|
||||
new[]
|
||||
{
|
||||
new CommandOptionInput("option1", "value1"),
|
||||
new CommandOptionInput("b", "value2")
|
||||
})
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {"--switch"},
|
||||
new CommandLineInput(
|
||||
new[]
|
||||
{
|
||||
new CommandOptionInput("switch")
|
||||
})
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {"--switch1", "--switch2"},
|
||||
new CommandLineInput(
|
||||
new[]
|
||||
{
|
||||
new CommandOptionInput("switch1"),
|
||||
new CommandOptionInput("switch2")
|
||||
})
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {"-s"},
|
||||
new CommandLineInput(
|
||||
new[]
|
||||
{
|
||||
new CommandOptionInput("s")
|
||||
})
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {"-a", "-b"},
|
||||
new CommandLineInput(
|
||||
new[]
|
||||
{
|
||||
new CommandOptionInput("a"),
|
||||
new CommandOptionInput("b")
|
||||
})
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {"-ab"},
|
||||
new CommandLineInput(
|
||||
new[]
|
||||
{
|
||||
new CommandOptionInput("a"),
|
||||
new CommandOptionInput("b")
|
||||
})
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {"-ab", "value"},
|
||||
new CommandLineInput(
|
||||
new[]
|
||||
{
|
||||
new CommandOptionInput("a"),
|
||||
new CommandOptionInput("b", "value")
|
||||
})
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {"cmd", "--option", "value"},
|
||||
new CommandLineInput(
|
||||
new[] {"cmd"},
|
||||
new[]
|
||||
{
|
||||
new CommandOptionInput("option", "value")
|
||||
})
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {"[debug]"},
|
||||
new CommandLineInput(
|
||||
new[] {"debug"},
|
||||
new string[0],
|
||||
new CommandOptionInput[0])
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {"[debug]", "[preview]"},
|
||||
new CommandLineInput(
|
||||
new[] {"debug", "preview"},
|
||||
new string[0],
|
||||
new CommandOptionInput[0])
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {"cmd", "param1", "param2", "--option", "value"},
|
||||
new CommandLineInput(
|
||||
new[] {"cmd", "param1", "param2"},
|
||||
new[]
|
||||
{
|
||||
new CommandOptionInput("option", "value")
|
||||
})
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {"[debug]", "[preview]", "-o", "value"},
|
||||
new CommandLineInput(
|
||||
new[] {"debug", "preview"},
|
||||
new string[0],
|
||||
new[]
|
||||
{
|
||||
new CommandOptionInput("o", "value")
|
||||
})
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {"cmd", "[debug]", "[preview]", "-o", "value"},
|
||||
new CommandLineInput(
|
||||
new[] {"debug", "preview"},
|
||||
new[] {"cmd"},
|
||||
new[]
|
||||
{
|
||||
new CommandOptionInput("o", "value")
|
||||
})
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {"cmd", "[debug]", "[preview]", "-o", "value"},
|
||||
new CommandLineInput(
|
||||
new[] {"debug", "preview"},
|
||||
new[] {"cmd"},
|
||||
new[]
|
||||
{
|
||||
new CommandOptionInput("o", "value")
|
||||
})
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {"cmd", "param", "[debug]", "[preview]", "-o", "value"},
|
||||
new CommandLineInput(
|
||||
new[] {"debug", "preview"},
|
||||
new[] {"cmd", "param"},
|
||||
new[]
|
||||
{
|
||||
new CommandOptionInput("o", "value")
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
[Test]
|
||||
[TestCaseSource(nameof(GetTestCases_Parse))]
|
||||
public void Parse_Test(IReadOnlyList<string> commandLineArguments, CommandLineInput expectedResult)
|
||||
{
|
||||
// Act
|
||||
var result = CommandLineInput.Parse(commandLineArguments);
|
||||
|
||||
// Assert
|
||||
result.Should().BeEquivalentTo(expectedResult);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Threading.Tasks;
|
||||
using CliWrap;
|
||||
using FluentAssertions;
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace CliFx.Tests
|
||||
{
|
||||
[TestFixture]
|
||||
public class DummyTests
|
||||
{
|
||||
private static Assembly DummyAssembly { get; } = typeof(Dummy.Program).Assembly;
|
||||
|
||||
private static IEnumerable<TestCaseData> GetTestCases_RunAsync()
|
||||
{
|
||||
yield return new TestCaseData(
|
||||
new[] {"--version"},
|
||||
new Dictionary<string, string>(),
|
||||
$"v{DummyAssembly.GetName().Version}"
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new string[0],
|
||||
new Dictionary<string, string>(),
|
||||
"Hello World!"
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {"--target", "Earth"},
|
||||
new Dictionary<string, string>(),
|
||||
"Hello Earth!"
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new string[0],
|
||||
new Dictionary<string, string>
|
||||
{
|
||||
["ENV_TARGET"] = "Mars"
|
||||
},
|
||||
"Hello Mars!"
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {"--target", "Earth"},
|
||||
new Dictionary<string, string>
|
||||
{
|
||||
["ENV_TARGET"] = "Mars"
|
||||
},
|
||||
"Hello Earth!"
|
||||
);
|
||||
}
|
||||
|
||||
[TestCaseSource(nameof(GetTestCases_RunAsync))]
|
||||
public async Task RunAsync_Test(
|
||||
IReadOnlyList<string> arguments,
|
||||
IReadOnlyDictionary<string, string> environmentVariables,
|
||||
string expectedStdOut)
|
||||
{
|
||||
// Arrange
|
||||
var cli = Cli.Wrap("dotnet")
|
||||
.SetArguments(arguments.Prepend(DummyAssembly.Location).ToArray())
|
||||
.EnableExitCodeValidation()
|
||||
.EnableStandardErrorValidation()
|
||||
.SetStandardOutputCallback(Console.WriteLine)
|
||||
.SetStandardErrorCallback(Console.WriteLine);
|
||||
|
||||
foreach (var (key, value) in environmentVariables)
|
||||
cli.SetEnvironmentVariable(key, value);
|
||||
|
||||
// Act
|
||||
var result = await cli.ExecuteAsync();
|
||||
|
||||
// Assert
|
||||
result.ExitCode.Should().Be(0);
|
||||
result.StandardError.Should().BeNullOrWhiteSpace();
|
||||
result.StandardOutput.TrimEnd().Should().Be(expectedStdOut);
|
||||
}
|
||||
}
|
||||
}
|
||||
160
CliFx.Tests/EnvironmentSpecs.cs
Normal file
160
CliFx.Tests/EnvironmentSpecs.cs
Normal file
@@ -0,0 +1,160 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using CliFx.Tests.Utils;
|
||||
using CliFx.Tests.Utils.Extensions;
|
||||
using CliWrap;
|
||||
using CliWrap.Buffered;
|
||||
using FluentAssertions;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace CliFx.Tests;
|
||||
|
||||
public class EnvironmentSpecs(ITestOutputHelper testOutput) : SpecsBase(testOutput)
|
||||
{
|
||||
[Fact]
|
||||
public async Task I_can_configure_an_option_to_fall_back_to_an_environment_variable_if_the_user_does_not_provide_the_corresponding_argument()
|
||||
{
|
||||
// Arrange
|
||||
var commandType = DynamicCommandBuilder.Compile(
|
||||
// lang=csharp
|
||||
"""
|
||||
[Command]
|
||||
public class Command : ICommand
|
||||
{
|
||||
[CommandOption("foo", EnvironmentVariable = "ENV_FOO")]
|
||||
public string? Foo { get; init; }
|
||||
|
||||
[CommandOption("bar", EnvironmentVariable = "ENV_BAR")]
|
||||
public string? Bar { get; init; }
|
||||
|
||||
public ValueTask ExecuteAsync(IConsole console)
|
||||
{
|
||||
console.WriteLine(Foo);
|
||||
console.WriteLine(Bar);
|
||||
|
||||
return default;
|
||||
}
|
||||
}
|
||||
"""
|
||||
);
|
||||
|
||||
var application = new CliApplicationBuilder()
|
||||
.AddCommand(commandType)
|
||||
.UseConsole(FakeConsole)
|
||||
.Build();
|
||||
|
||||
// Act
|
||||
var exitCode = await application.RunAsync(
|
||||
["--foo", "42"],
|
||||
new Dictionary<string, string> { ["ENV_FOO"] = "100", ["ENV_BAR"] = "200" }
|
||||
);
|
||||
|
||||
// Assert
|
||||
exitCode.Should().Be(0);
|
||||
|
||||
var stdOut = FakeConsole.ReadOutputString();
|
||||
stdOut.Trim().Should().ConsistOfLines("42", "200");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task I_can_configure_an_option_bound_to_a_non_scalar_property_to_fall_back_to_an_environment_variable_if_the_user_does_not_provide_the_corresponding_argument()
|
||||
{
|
||||
// Arrange
|
||||
var commandType = DynamicCommandBuilder.Compile(
|
||||
// lang=csharp
|
||||
"""
|
||||
[Command]
|
||||
public class Command : ICommand
|
||||
{
|
||||
[CommandOption("foo", EnvironmentVariable = "ENV_FOO")]
|
||||
public IReadOnlyList<string>? Foo { get; init; }
|
||||
|
||||
public ValueTask ExecuteAsync(IConsole console)
|
||||
{
|
||||
foreach (var i in Foo)
|
||||
console.WriteLine(i);
|
||||
|
||||
return default;
|
||||
}
|
||||
}
|
||||
"""
|
||||
);
|
||||
|
||||
var application = new CliApplicationBuilder()
|
||||
.AddCommand(commandType)
|
||||
.UseConsole(FakeConsole)
|
||||
.Build();
|
||||
|
||||
// Act
|
||||
var exitCode = await application.RunAsync(
|
||||
[],
|
||||
new Dictionary<string, string> { ["ENV_FOO"] = $"bar{Path.PathSeparator}baz" }
|
||||
);
|
||||
|
||||
// Assert
|
||||
exitCode.Should().Be(0);
|
||||
|
||||
var stdOut = FakeConsole.ReadOutputString();
|
||||
stdOut.Should().ConsistOfLines("bar", "baz");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task I_can_configure_an_option_bound_to_a_scalar_property_to_fall_back_to_an_environment_variable_while_ignoring_path_separators()
|
||||
{
|
||||
// Arrange
|
||||
var commandType = DynamicCommandBuilder.Compile(
|
||||
// lang=csharp
|
||||
"""
|
||||
[Command]
|
||||
public class Command : ICommand
|
||||
{
|
||||
[CommandOption("foo", EnvironmentVariable = "ENV_FOO")]
|
||||
public string? Foo { get; init; }
|
||||
|
||||
public ValueTask ExecuteAsync(IConsole console)
|
||||
{
|
||||
console.WriteLine(Foo);
|
||||
return default;
|
||||
}
|
||||
}
|
||||
"""
|
||||
);
|
||||
|
||||
var application = new CliApplicationBuilder()
|
||||
.AddCommand(commandType)
|
||||
.UseConsole(FakeConsole)
|
||||
.Build();
|
||||
|
||||
// Act
|
||||
var exitCode = await application.RunAsync(
|
||||
[],
|
||||
new Dictionary<string, string> { ["ENV_FOO"] = $"bar{Path.PathSeparator}baz" }
|
||||
);
|
||||
|
||||
// Assert
|
||||
exitCode.Should().Be(0);
|
||||
|
||||
var stdOut = FakeConsole.ReadOutputString();
|
||||
stdOut.Trim().Should().Be($"bar{Path.PathSeparator}baz");
|
||||
}
|
||||
|
||||
[Fact(Timeout = 15000)]
|
||||
public async Task I_can_run_the_application_and_it_will_resolve_all_required_environment_variables_automatically()
|
||||
{
|
||||
// Ensures that the environment variables are properly obtained from
|
||||
// System.Environment when they are not provided explicitly to CliApplication.
|
||||
|
||||
// Arrange
|
||||
var command = Cli.Wrap(Dummy.Program.FilePath)
|
||||
.WithArguments("env-test")
|
||||
.WithEnvironmentVariables(e => e.Set("ENV_TARGET", "Mars"));
|
||||
|
||||
// Act
|
||||
var result = await command.ExecuteBufferedAsync();
|
||||
|
||||
// Assert
|
||||
result.StandardOutput.Trim().Should().Be("Hello Mars!");
|
||||
}
|
||||
}
|
||||
209
CliFx.Tests/ErrorReportingSpecs.cs
Normal file
209
CliFx.Tests/ErrorReportingSpecs.cs
Normal file
@@ -0,0 +1,209 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using CliFx.Tests.Utils;
|
||||
using CliFx.Tests.Utils.Extensions;
|
||||
using FluentAssertions;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace CliFx.Tests;
|
||||
|
||||
public class ErrorReportingSpecs(ITestOutputHelper testOutput) : SpecsBase(testOutput)
|
||||
{
|
||||
[Fact]
|
||||
public async Task I_can_throw_an_exception_in_a_command_to_report_an_error_with_a_stacktrace()
|
||||
{
|
||||
// Arrange
|
||||
var commandType = DynamicCommandBuilder.Compile(
|
||||
// lang=csharp
|
||||
"""
|
||||
[Command]
|
||||
public class Command : ICommand
|
||||
{
|
||||
public ValueTask ExecuteAsync(IConsole console) =>
|
||||
throw new Exception("Something went wrong");
|
||||
}
|
||||
"""
|
||||
);
|
||||
|
||||
var application = new CliApplicationBuilder()
|
||||
.AddCommand(commandType)
|
||||
.UseConsole(FakeConsole)
|
||||
.Build();
|
||||
|
||||
// Act
|
||||
var exitCode = await application.RunAsync(
|
||||
[],
|
||||
new Dictionary<string, string>()
|
||||
);
|
||||
|
||||
// Assert
|
||||
exitCode.Should().NotBe(0);
|
||||
|
||||
var stdOut = FakeConsole.ReadOutputString();
|
||||
stdOut.Should().BeEmpty();
|
||||
|
||||
var stdErr = FakeConsole.ReadErrorString();
|
||||
stdErr
|
||||
.Should()
|
||||
.ContainAllInOrder("System.Exception", "Something went wrong", "at", "CliFx.");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task I_can_throw_an_exception_with_an_inner_exception_in_a_command_to_report_an_error_with_a_stacktrace()
|
||||
{
|
||||
// Arrange
|
||||
var commandType = DynamicCommandBuilder.Compile(
|
||||
// lang=csharp
|
||||
"""
|
||||
[Command]
|
||||
public class Command : ICommand
|
||||
{
|
||||
public ValueTask ExecuteAsync(IConsole console) =>
|
||||
throw new Exception("Something went wrong", new Exception("Another exception"));
|
||||
}
|
||||
"""
|
||||
);
|
||||
|
||||
var application = new CliApplicationBuilder()
|
||||
.AddCommand(commandType)
|
||||
.UseConsole(FakeConsole)
|
||||
.Build();
|
||||
|
||||
// Act
|
||||
var exitCode = await application.RunAsync(
|
||||
[],
|
||||
new Dictionary<string, string>()
|
||||
);
|
||||
|
||||
// Assert
|
||||
exitCode.Should().NotBe(0);
|
||||
|
||||
var stdOut = FakeConsole.ReadOutputString();
|
||||
stdOut.Should().BeEmpty();
|
||||
|
||||
var stdErr = FakeConsole.ReadErrorString();
|
||||
stdErr
|
||||
.Should()
|
||||
.ContainAllInOrder(
|
||||
"System.Exception",
|
||||
"Something went wrong",
|
||||
"System.Exception",
|
||||
"Another exception",
|
||||
"at",
|
||||
"CliFx."
|
||||
);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task I_can_throw_an_exception_in_a_command_to_report_an_error_and_exit_with_the_specified_code()
|
||||
{
|
||||
// Arrange
|
||||
var commandType = DynamicCommandBuilder.Compile(
|
||||
// lang=csharp
|
||||
"""
|
||||
[Command]
|
||||
public class Command : ICommand
|
||||
{
|
||||
public ValueTask ExecuteAsync(IConsole console) =>
|
||||
throw new CommandException("Something went wrong", 69);
|
||||
}
|
||||
"""
|
||||
);
|
||||
|
||||
var application = new CliApplicationBuilder()
|
||||
.AddCommand(commandType)
|
||||
.UseConsole(FakeConsole)
|
||||
.Build();
|
||||
|
||||
// Act
|
||||
var exitCode = await application.RunAsync(
|
||||
[],
|
||||
new Dictionary<string, string>()
|
||||
);
|
||||
|
||||
// Assert
|
||||
exitCode.Should().Be(69);
|
||||
|
||||
var stdOut = FakeConsole.ReadOutputString();
|
||||
stdOut.Should().BeEmpty();
|
||||
|
||||
var stdErr = FakeConsole.ReadErrorString();
|
||||
stdErr.Trim().Should().Be("Something went wrong");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task I_can_throw_an_exception_without_a_message_in_a_command_to_report_an_error_with_a_stacktrace()
|
||||
{
|
||||
// Arrange
|
||||
var commandType = DynamicCommandBuilder.Compile(
|
||||
// lang=csharp
|
||||
"""
|
||||
[Command]
|
||||
public class Command : ICommand
|
||||
{
|
||||
public ValueTask ExecuteAsync(IConsole console) =>
|
||||
throw new CommandException("", 69);
|
||||
}
|
||||
"""
|
||||
);
|
||||
|
||||
var application = new CliApplicationBuilder()
|
||||
.AddCommand(commandType)
|
||||
.UseConsole(FakeConsole)
|
||||
.Build();
|
||||
|
||||
// Act
|
||||
var exitCode = await application.RunAsync(
|
||||
[],
|
||||
new Dictionary<string, string>()
|
||||
);
|
||||
|
||||
// Assert
|
||||
exitCode.Should().Be(69);
|
||||
|
||||
var stdOut = FakeConsole.ReadOutputString();
|
||||
stdOut.Should().BeEmpty();
|
||||
|
||||
var stdErr = FakeConsole.ReadErrorString();
|
||||
stdErr.Should().ContainAllInOrder("CliFx.Exceptions.CommandException", "at", "CliFx.");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task I_can_throw_an_exception_in_a_command_to_report_an_error_and_print_the_help_text()
|
||||
{
|
||||
// Arrange
|
||||
var commandType = DynamicCommandBuilder.Compile(
|
||||
// lang=csharp
|
||||
"""
|
||||
[Command]
|
||||
public class Command : ICommand
|
||||
{
|
||||
public ValueTask ExecuteAsync(IConsole console) =>
|
||||
throw new CommandException("Something went wrong", 69, true);
|
||||
}
|
||||
"""
|
||||
);
|
||||
|
||||
var application = new CliApplicationBuilder()
|
||||
.AddCommand(commandType)
|
||||
.UseConsole(FakeConsole)
|
||||
.SetDescription("This will be in help text")
|
||||
.Build();
|
||||
|
||||
// Act
|
||||
var exitCode = await application.RunAsync(
|
||||
[],
|
||||
new Dictionary<string, string>()
|
||||
);
|
||||
|
||||
// Assert
|
||||
exitCode.Should().Be(69);
|
||||
|
||||
var stdOut = FakeConsole.ReadOutputString();
|
||||
stdOut.Should().Contain("This will be in help text");
|
||||
|
||||
var stdErr = FakeConsole.ReadErrorString();
|
||||
stdErr.Trim().Should().Be("Something went wrong");
|
||||
}
|
||||
}
|
||||
1062
CliFx.Tests/HelpTextSpecs.cs
Normal file
1062
CliFx.Tests/HelpTextSpecs.cs
Normal file
File diff suppressed because it is too large
Load Diff
842
CliFx.Tests/OptionBindingSpecs.cs
Normal file
842
CliFx.Tests/OptionBindingSpecs.cs
Normal file
@@ -0,0 +1,842 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using CliFx.Tests.Utils;
|
||||
using CliFx.Tests.Utils.Extensions;
|
||||
using FluentAssertions;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace CliFx.Tests;
|
||||
|
||||
public class OptionBindingSpecs(ITestOutputHelper testOutput) : SpecsBase(testOutput)
|
||||
{
|
||||
[Fact]
|
||||
public async Task I_can_bind_an_option_to_a_property_and_get_the_value_from_the_corresponding_argument_by_name()
|
||||
{
|
||||
// Arrange
|
||||
var commandType = DynamicCommandBuilder.Compile(
|
||||
// lang=csharp
|
||||
"""
|
||||
[Command]
|
||||
public class Command : ICommand
|
||||
{
|
||||
[CommandOption("foo")]
|
||||
public bool Foo { get; init; }
|
||||
|
||||
public ValueTask ExecuteAsync(IConsole console)
|
||||
{
|
||||
console.WriteLine(Foo);
|
||||
return default;
|
||||
}
|
||||
}
|
||||
"""
|
||||
);
|
||||
|
||||
var application = new CliApplicationBuilder()
|
||||
.AddCommand(commandType)
|
||||
.UseConsole(FakeConsole)
|
||||
.Build();
|
||||
|
||||
// Act
|
||||
var exitCode = await application.RunAsync(["--foo"], new Dictionary<string, string>());
|
||||
|
||||
// Assert
|
||||
exitCode.Should().Be(0);
|
||||
|
||||
var stdOut = FakeConsole.ReadOutputString();
|
||||
stdOut.Trim().Should().Be("True");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task I_can_bind_an_option_to_a_property_and_get_the_value_from_the_corresponding_argument_by_short_name()
|
||||
{
|
||||
// Arrange
|
||||
var commandType = DynamicCommandBuilder.Compile(
|
||||
// lang=csharp
|
||||
"""
|
||||
[Command]
|
||||
public class Command : ICommand
|
||||
{
|
||||
[CommandOption('f')]
|
||||
public bool Foo { get; init; }
|
||||
|
||||
public ValueTask ExecuteAsync(IConsole console)
|
||||
{
|
||||
console.WriteLine(Foo);
|
||||
return default;
|
||||
}
|
||||
}
|
||||
"""
|
||||
);
|
||||
|
||||
var application = new CliApplicationBuilder()
|
||||
.AddCommand(commandType)
|
||||
.UseConsole(FakeConsole)
|
||||
.Build();
|
||||
|
||||
// Act
|
||||
var exitCode = await application.RunAsync(["-f"], new Dictionary<string, string>());
|
||||
|
||||
// Assert
|
||||
exitCode.Should().Be(0);
|
||||
|
||||
var stdOut = FakeConsole.ReadOutputString();
|
||||
stdOut.Trim().Should().Be("True");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task I_can_bind_an_option_to_a_property_and_get_the_value_from_the_corresponding_argument_set_by_name()
|
||||
{
|
||||
// Arrange
|
||||
var commandType = DynamicCommandBuilder.Compile(
|
||||
// lang=csharp
|
||||
"""
|
||||
[Command]
|
||||
public class Command : ICommand
|
||||
{
|
||||
[CommandOption("foo")]
|
||||
public string? Foo { get; init; }
|
||||
|
||||
[CommandOption("bar")]
|
||||
public string? Bar { get; init; }
|
||||
|
||||
public ValueTask ExecuteAsync(IConsole console)
|
||||
{
|
||||
console.WriteLine("Foo = " + Foo);
|
||||
console.WriteLine("Bar = " + Bar);
|
||||
|
||||
return default;
|
||||
}
|
||||
}
|
||||
"""
|
||||
);
|
||||
|
||||
var application = new CliApplicationBuilder()
|
||||
.AddCommand(commandType)
|
||||
.UseConsole(FakeConsole)
|
||||
.Build();
|
||||
|
||||
// Act
|
||||
var exitCode = await application.RunAsync(
|
||||
["--foo", "one", "--bar", "two"],
|
||||
new Dictionary<string, string>()
|
||||
);
|
||||
|
||||
// Assert
|
||||
exitCode.Should().Be(0);
|
||||
|
||||
var stdOut = FakeConsole.ReadOutputString();
|
||||
stdOut.Should().ConsistOfLines("Foo = one", "Bar = two");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task I_can_bind_an_option_to_a_property_and_get_the_value_from_the_corresponding_argument_set_by_short_name()
|
||||
{
|
||||
// Arrange
|
||||
var commandType = DynamicCommandBuilder.Compile(
|
||||
// lang=csharp
|
||||
"""
|
||||
[Command]
|
||||
public class Command : ICommand
|
||||
{
|
||||
[CommandOption('f')]
|
||||
public string? Foo { get; init; }
|
||||
|
||||
[CommandOption('b')]
|
||||
public string? Bar { get; init; }
|
||||
|
||||
public ValueTask ExecuteAsync(IConsole console)
|
||||
{
|
||||
console.WriteLine("Foo = " + Foo);
|
||||
console.WriteLine("Bar = " + Bar);
|
||||
|
||||
return default;
|
||||
}
|
||||
}
|
||||
"""
|
||||
);
|
||||
|
||||
var application = new CliApplicationBuilder()
|
||||
.AddCommand(commandType)
|
||||
.UseConsole(FakeConsole)
|
||||
.Build();
|
||||
|
||||
// Act
|
||||
var exitCode = await application.RunAsync(
|
||||
["-f", "one", "-b", "two"],
|
||||
new Dictionary<string, string>()
|
||||
);
|
||||
|
||||
// Assert
|
||||
exitCode.Should().Be(0);
|
||||
|
||||
var stdOut = FakeConsole.ReadOutputString();
|
||||
stdOut.Should().ConsistOfLines("Foo = one", "Bar = two");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task I_can_bind_an_option_to_a_property_and_get_the_value_from_the_corresponding_argument_stack_by_short_name()
|
||||
{
|
||||
// Arrange
|
||||
var commandType = DynamicCommandBuilder.Compile(
|
||||
// lang=csharp
|
||||
"""
|
||||
[Command]
|
||||
public class Command : ICommand
|
||||
{
|
||||
[CommandOption('f')]
|
||||
public string? Foo { get; init; }
|
||||
|
||||
[CommandOption('b')]
|
||||
public string? Bar { get; init; }
|
||||
|
||||
public ValueTask ExecuteAsync(IConsole console)
|
||||
{
|
||||
console.WriteLine("Foo = " + Foo);
|
||||
console.WriteLine("Bar = " + Bar);
|
||||
|
||||
return default;
|
||||
}
|
||||
}
|
||||
"""
|
||||
);
|
||||
|
||||
var application = new CliApplicationBuilder()
|
||||
.AddCommand(commandType)
|
||||
.UseConsole(FakeConsole)
|
||||
.Build();
|
||||
|
||||
// Act
|
||||
var exitCode = await application.RunAsync(
|
||||
["-fb", "value"],
|
||||
new Dictionary<string, string>()
|
||||
);
|
||||
|
||||
// Assert
|
||||
exitCode.Should().Be(0);
|
||||
|
||||
var stdOut = FakeConsole.ReadOutputString();
|
||||
stdOut.Should().ConsistOfLines("Foo = ", "Bar = value");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task I_can_bind_an_option_to_a_non_scalar_property_and_get_the_values_from_the_corresponding_arguments_by_name()
|
||||
{
|
||||
// Arrange
|
||||
var commandType = DynamicCommandBuilder.Compile(
|
||||
// lang=csharp
|
||||
"""
|
||||
[Command]
|
||||
public class Command : ICommand
|
||||
{
|
||||
[CommandOption("Foo")]
|
||||
public IReadOnlyList<string>? Foo { get; init; }
|
||||
|
||||
public ValueTask ExecuteAsync(IConsole console)
|
||||
{
|
||||
foreach (var i in Foo)
|
||||
console.WriteLine(i);
|
||||
|
||||
return default;
|
||||
}
|
||||
}
|
||||
"""
|
||||
);
|
||||
|
||||
var application = new CliApplicationBuilder()
|
||||
.AddCommand(commandType)
|
||||
.UseConsole(FakeConsole)
|
||||
.Build();
|
||||
|
||||
// Act
|
||||
var exitCode = await application.RunAsync(
|
||||
["--foo", "one", "two", "three"],
|
||||
new Dictionary<string, string>()
|
||||
);
|
||||
|
||||
// Assert
|
||||
exitCode.Should().Be(0);
|
||||
|
||||
var stdOut = FakeConsole.ReadOutputString();
|
||||
stdOut.Should().ConsistOfLines("one", "two", "three");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task I_can_bind_an_option_to_a_non_scalar_property_and_get_the_values_from_the_corresponding_arguments_by_short_name()
|
||||
{
|
||||
// Arrange
|
||||
var commandType = DynamicCommandBuilder.Compile(
|
||||
// lang=csharp
|
||||
"""
|
||||
[Command]
|
||||
public class Command : ICommand
|
||||
{
|
||||
[CommandOption('f')]
|
||||
public IReadOnlyList<string>? Foo { get; init; }
|
||||
|
||||
public ValueTask ExecuteAsync(IConsole console)
|
||||
{
|
||||
foreach (var i in Foo)
|
||||
console.WriteLine(i);
|
||||
|
||||
return default;
|
||||
}
|
||||
}
|
||||
"""
|
||||
);
|
||||
|
||||
var application = new CliApplicationBuilder()
|
||||
.AddCommand(commandType)
|
||||
.UseConsole(FakeConsole)
|
||||
.Build();
|
||||
|
||||
// Act
|
||||
var exitCode = await application.RunAsync(
|
||||
["-f", "one", "two", "three"],
|
||||
new Dictionary<string, string>()
|
||||
);
|
||||
|
||||
// Assert
|
||||
exitCode.Should().Be(0);
|
||||
|
||||
var stdOut = FakeConsole.ReadOutputString();
|
||||
stdOut.Should().ConsistOfLines("one", "two", "three");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task I_can_bind_an_option_to_a_non_scalar_property_and_get_the_values_from_the_corresponding_argument_sets_by_name()
|
||||
{
|
||||
// Arrange
|
||||
var commandType = DynamicCommandBuilder.Compile(
|
||||
// lang=csharp
|
||||
"""
|
||||
[Command]
|
||||
public class Command : ICommand
|
||||
{
|
||||
[CommandOption("foo")]
|
||||
public IReadOnlyList<string>? Foo { get; init; }
|
||||
|
||||
public ValueTask ExecuteAsync(IConsole console)
|
||||
{
|
||||
foreach (var i in Foo)
|
||||
console.WriteLine(i);
|
||||
|
||||
return default;
|
||||
}
|
||||
}
|
||||
"""
|
||||
);
|
||||
|
||||
var application = new CliApplicationBuilder()
|
||||
.AddCommand(commandType)
|
||||
.UseConsole(FakeConsole)
|
||||
.Build();
|
||||
|
||||
// Act
|
||||
var exitCode = await application.RunAsync(
|
||||
["--foo", "one", "--foo", "two", "--foo", "three"],
|
||||
new Dictionary<string, string>()
|
||||
);
|
||||
|
||||
// Assert
|
||||
exitCode.Should().Be(0);
|
||||
|
||||
var stdOut = FakeConsole.ReadOutputString();
|
||||
stdOut.Should().ConsistOfLines("one", "two", "three");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task I_can_bind_an_option_to_a_non_scalar_property_and_get_the_values_from_the_corresponding_argument_sets_by_short_name()
|
||||
{
|
||||
// Arrange
|
||||
var commandType = DynamicCommandBuilder.Compile(
|
||||
// lang=csharp
|
||||
"""
|
||||
[Command]
|
||||
public class Command : ICommand
|
||||
{
|
||||
[CommandOption('f')]
|
||||
public IReadOnlyList<string>? Foo { get; init; }
|
||||
|
||||
public ValueTask ExecuteAsync(IConsole console)
|
||||
{
|
||||
foreach (var i in Foo)
|
||||
console.WriteLine(i);
|
||||
|
||||
return default;
|
||||
}
|
||||
}
|
||||
"""
|
||||
);
|
||||
|
||||
var application = new CliApplicationBuilder()
|
||||
.AddCommand(commandType)
|
||||
.UseConsole(FakeConsole)
|
||||
.Build();
|
||||
|
||||
// Act
|
||||
var exitCode = await application.RunAsync(
|
||||
["-f", "one", "-f", "two", "-f", "three"],
|
||||
new Dictionary<string, string>()
|
||||
);
|
||||
|
||||
// Assert
|
||||
exitCode.Should().Be(0);
|
||||
|
||||
var stdOut = FakeConsole.ReadOutputString();
|
||||
stdOut.Should().ConsistOfLines("one", "two", "three");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task I_can_bind_an_option_to_a_non_scalar_property_and_get_the_values_from_the_corresponding_argument_sets_by_name_or_short_name()
|
||||
{
|
||||
// Arrange
|
||||
var commandType = DynamicCommandBuilder.Compile(
|
||||
// lang=csharp
|
||||
"""
|
||||
[Command]
|
||||
public class Command : ICommand
|
||||
{
|
||||
[CommandOption("foo", 'f')]
|
||||
public IReadOnlyList<string>? Foo { get; init; }
|
||||
|
||||
public ValueTask ExecuteAsync(IConsole console)
|
||||
{
|
||||
foreach (var i in Foo)
|
||||
console.WriteLine(i);
|
||||
|
||||
return default;
|
||||
}
|
||||
}
|
||||
"""
|
||||
);
|
||||
|
||||
var application = new CliApplicationBuilder()
|
||||
.AddCommand(commandType)
|
||||
.UseConsole(FakeConsole)
|
||||
.Build();
|
||||
|
||||
// Act
|
||||
var exitCode = await application.RunAsync(
|
||||
["--foo", "one", "-f", "two", "--foo", "three"],
|
||||
new Dictionary<string, string>()
|
||||
);
|
||||
|
||||
// Assert
|
||||
exitCode.Should().Be(0);
|
||||
|
||||
var stdOut = FakeConsole.ReadOutputString();
|
||||
stdOut.Should().ConsistOfLines("one", "two", "three");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task I_can_bind_an_option_to_a_property_and_get_no_value_if_the_user_does_not_provide_the_corresponding_argument()
|
||||
{
|
||||
// Arrange
|
||||
var commandType = DynamicCommandBuilder.Compile(
|
||||
// lang=csharp
|
||||
"""
|
||||
[Command]
|
||||
public class Command : ICommand
|
||||
{
|
||||
[CommandOption("foo")]
|
||||
public string? Foo { get; init; }
|
||||
|
||||
[CommandOption("bar")]
|
||||
public string? Bar { get; init; } = "hello";
|
||||
|
||||
public ValueTask ExecuteAsync(IConsole console)
|
||||
{
|
||||
console.WriteLine("Foo = " + Foo);
|
||||
console.WriteLine("Bar = " + Bar);
|
||||
|
||||
return default;
|
||||
}
|
||||
}
|
||||
"""
|
||||
);
|
||||
|
||||
var application = new CliApplicationBuilder()
|
||||
.AddCommand(commandType)
|
||||
.UseConsole(FakeConsole)
|
||||
.Build();
|
||||
|
||||
// Act
|
||||
var exitCode = await application.RunAsync(
|
||||
["--foo", "one"],
|
||||
new Dictionary<string, string>()
|
||||
);
|
||||
|
||||
// Assert
|
||||
exitCode.Should().Be(0);
|
||||
|
||||
var stdOut = FakeConsole.ReadOutputString();
|
||||
stdOut.Should().ConsistOfLines("Foo = one", "Bar = hello");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task I_can_bind_an_option_to_a_property_through_multiple_inheritance()
|
||||
{
|
||||
// Arrange
|
||||
var commandType = DynamicCommandBuilder.Compile(
|
||||
// lang=csharp
|
||||
"""
|
||||
public static class SharedContext
|
||||
{
|
||||
public static int Foo { get; set; }
|
||||
|
||||
public static bool Bar { get; set; }
|
||||
}
|
||||
|
||||
public interface IHasFoo : ICommand
|
||||
{
|
||||
[CommandOption("foo")]
|
||||
public int Foo
|
||||
{
|
||||
get => SharedContext.Foo;
|
||||
init => SharedContext.Foo = value;
|
||||
}
|
||||
}
|
||||
|
||||
public interface IHasBar : ICommand
|
||||
{
|
||||
[CommandOption("bar")]
|
||||
public bool Bar
|
||||
{
|
||||
get => SharedContext.Bar;
|
||||
init => SharedContext.Bar = value;
|
||||
}
|
||||
}
|
||||
|
||||
public interface IHasBaz : ICommand
|
||||
{
|
||||
public string? Baz { get; init; }
|
||||
}
|
||||
|
||||
[Command]
|
||||
public class Command : IHasFoo, IHasBar, IHasBaz
|
||||
{
|
||||
[CommandOption("baz")]
|
||||
public string? Baz { get; init; }
|
||||
|
||||
public ValueTask ExecuteAsync(IConsole console)
|
||||
{
|
||||
console.WriteLine("Foo = " + SharedContext.Foo);
|
||||
console.WriteLine("Bar = " + SharedContext.Bar);
|
||||
console.WriteLine("Baz = " + Baz);
|
||||
|
||||
return default;
|
||||
}
|
||||
}
|
||||
"""
|
||||
);
|
||||
|
||||
var application = new CliApplicationBuilder()
|
||||
.AddCommand(commandType)
|
||||
.UseConsole(FakeConsole)
|
||||
.Build();
|
||||
|
||||
// Act
|
||||
var exitCode = await application.RunAsync(["--foo", "42", "--bar", "--baz", "xyz"]);
|
||||
|
||||
// Assert
|
||||
exitCode.Should().Be(0);
|
||||
|
||||
var stdOut = FakeConsole.ReadOutputString();
|
||||
stdOut.Should().ConsistOfLines("Foo = 42", "Bar = True", "Baz = xyz");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task I_can_bind_an_option_to_a_property_and_get_the_correct_value_if_the_user_provides_an_argument_containing_a_negative_number()
|
||||
{
|
||||
// Arrange
|
||||
var commandType = DynamicCommandBuilder.Compile(
|
||||
// lang=csharp
|
||||
"""
|
||||
[Command]
|
||||
public class Command : ICommand
|
||||
{
|
||||
[CommandOption("foo")]
|
||||
public string? Foo { get; init; }
|
||||
|
||||
public ValueTask ExecuteAsync(IConsole console)
|
||||
{
|
||||
console.WriteLine(Foo);
|
||||
return default;
|
||||
}
|
||||
}
|
||||
"""
|
||||
);
|
||||
|
||||
var application = new CliApplicationBuilder()
|
||||
.AddCommand(commandType)
|
||||
.UseConsole(FakeConsole)
|
||||
.Build();
|
||||
|
||||
// Act
|
||||
var exitCode = await application.RunAsync(
|
||||
["--foo", "-13"],
|
||||
new Dictionary<string, string>()
|
||||
);
|
||||
|
||||
// Assert
|
||||
exitCode.Should().Be(0);
|
||||
|
||||
var stdOut = FakeConsole.ReadOutputString();
|
||||
stdOut.Trim().Should().Be("-13");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task I_can_bind_an_option_to_a_property_with_the_same_identifier_as_the_implicit_help_option_and_get_the_correct_value()
|
||||
{
|
||||
// Arrange
|
||||
var commandType = DynamicCommandBuilder.Compile(
|
||||
// lang=csharp
|
||||
"""
|
||||
[Command]
|
||||
public class Command : ICommand
|
||||
{
|
||||
[CommandOption("help", 'h')]
|
||||
public string? Foo { get; init; }
|
||||
|
||||
public ValueTask ExecuteAsync(IConsole console)
|
||||
{
|
||||
console.WriteLine(Foo);
|
||||
return default;
|
||||
}
|
||||
}
|
||||
"""
|
||||
);
|
||||
|
||||
var application = new CliApplicationBuilder()
|
||||
.AddCommand(commandType)
|
||||
.UseConsole(FakeConsole)
|
||||
.Build();
|
||||
|
||||
// Act
|
||||
var exitCode = await application.RunAsync(
|
||||
["--help", "me"],
|
||||
new Dictionary<string, string>()
|
||||
);
|
||||
|
||||
// Assert
|
||||
exitCode.Should().Be(0);
|
||||
|
||||
var stdOut = FakeConsole.ReadOutputString();
|
||||
stdOut.Trim().Should().Be("me");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task I_can_bind_an_option_to_a_property_with_the_same_identifier_as_the_implicit_version_option_and_get_the_correct_value()
|
||||
{
|
||||
// Arrange
|
||||
var commandType = DynamicCommandBuilder.Compile(
|
||||
// lang=csharp
|
||||
"""
|
||||
[Command]
|
||||
public class Command : ICommand
|
||||
{
|
||||
[CommandOption("version")]
|
||||
public string? Foo { get; init; }
|
||||
|
||||
public ValueTask ExecuteAsync(IConsole console)
|
||||
{
|
||||
console.WriteLine(Foo);
|
||||
return default;
|
||||
}
|
||||
}
|
||||
"""
|
||||
);
|
||||
|
||||
var application = new CliApplicationBuilder()
|
||||
.AddCommand(commandType)
|
||||
.UseConsole(FakeConsole)
|
||||
.Build();
|
||||
|
||||
// Act
|
||||
var exitCode = await application.RunAsync(
|
||||
["--version", "1.2.0"],
|
||||
new Dictionary<string, string>()
|
||||
);
|
||||
|
||||
// Assert
|
||||
exitCode.Should().Be(0);
|
||||
|
||||
var stdOut = FakeConsole.ReadOutputString();
|
||||
stdOut.Trim().Should().Be("1.2.0");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task I_can_try_to_bind_a_required_option_to_a_property_and_get_an_error_if_the_user_does_not_provide_the_corresponding_argument()
|
||||
{
|
||||
// Arrange
|
||||
var commandType = DynamicCommandBuilder.Compile(
|
||||
// lang=csharp
|
||||
"""
|
||||
[Command]
|
||||
public class Command : ICommand
|
||||
{
|
||||
[CommandOption("foo")]
|
||||
public required string Foo { get; init; }
|
||||
|
||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||
}
|
||||
"""
|
||||
);
|
||||
|
||||
var application = new CliApplicationBuilder()
|
||||
.AddCommand(commandType)
|
||||
.UseConsole(FakeConsole)
|
||||
.Build();
|
||||
|
||||
// Act
|
||||
var exitCode = await application.RunAsync(
|
||||
[],
|
||||
new Dictionary<string, string>()
|
||||
);
|
||||
|
||||
// Assert
|
||||
exitCode.Should().NotBe(0);
|
||||
|
||||
var stdErr = FakeConsole.ReadErrorString();
|
||||
stdErr.Should().Contain("Missing required option(s)");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task I_can_try_to_bind_a_required_option_to_a_property_and_get_an_error_if_the_user_provides_an_empty_argument()
|
||||
{
|
||||
// Arrange
|
||||
var commandType = DynamicCommandBuilder.Compile(
|
||||
// lang=csharp
|
||||
"""
|
||||
[Command]
|
||||
public class Command : ICommand
|
||||
{
|
||||
[CommandOption("foo")]
|
||||
public required string Foo { get; init; }
|
||||
|
||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||
}
|
||||
"""
|
||||
);
|
||||
|
||||
var application = new CliApplicationBuilder()
|
||||
.AddCommand(commandType)
|
||||
.UseConsole(FakeConsole)
|
||||
.Build();
|
||||
|
||||
// Act
|
||||
var exitCode = await application.RunAsync(["--foo"], new Dictionary<string, string>());
|
||||
|
||||
// Assert
|
||||
exitCode.Should().NotBe(0);
|
||||
|
||||
var stdErr = FakeConsole.ReadErrorString();
|
||||
stdErr.Should().Contain("Missing required option(s)");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task I_can_try_to_bind_an_option_to_a_non_scalar_property_and_get_an_error_if_the_user_does_not_provide_at_least_one_corresponding_argument()
|
||||
{
|
||||
// Arrange
|
||||
var commandType = DynamicCommandBuilder.Compile(
|
||||
// lang=csharp
|
||||
"""
|
||||
[Command]
|
||||
public class Command : ICommand
|
||||
{
|
||||
[CommandOption("foo")]
|
||||
public required IReadOnlyList<string> Foo { get; init; }
|
||||
|
||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||
}
|
||||
"""
|
||||
);
|
||||
|
||||
var application = new CliApplicationBuilder()
|
||||
.AddCommand(commandType)
|
||||
.UseConsole(FakeConsole)
|
||||
.Build();
|
||||
|
||||
// Act
|
||||
var exitCode = await application.RunAsync(["--foo"], new Dictionary<string, string>());
|
||||
|
||||
// Assert
|
||||
exitCode.Should().NotBe(0);
|
||||
|
||||
var stdErr = FakeConsole.ReadErrorString();
|
||||
stdErr.Should().Contain("Missing required option(s)");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task I_can_try_to_bind_options_and_get_an_error_if_the_user_provides_unrecognized_arguments()
|
||||
{
|
||||
// Arrange
|
||||
var commandType = DynamicCommandBuilder.Compile(
|
||||
// lang=csharp
|
||||
"""
|
||||
[Command]
|
||||
public class Command : ICommand
|
||||
{
|
||||
[CommandOption("foo")]
|
||||
public string? Foo { get; init; }
|
||||
|
||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||
}
|
||||
"""
|
||||
);
|
||||
|
||||
var application = new CliApplicationBuilder()
|
||||
.AddCommand(commandType)
|
||||
.UseConsole(FakeConsole)
|
||||
.Build();
|
||||
|
||||
// Act
|
||||
var exitCode = await application.RunAsync(
|
||||
["--foo", "one", "--bar", "two"],
|
||||
new Dictionary<string, string>()
|
||||
);
|
||||
|
||||
// Assert
|
||||
exitCode.Should().NotBe(0);
|
||||
|
||||
var stdErr = FakeConsole.ReadErrorString();
|
||||
stdErr.Should().Contain("Unrecognized option(s)");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task I_can_try_to_bind_an_option_to_a_scalar_property_and_get_an_error_if_the_user_provides_too_many_arguments()
|
||||
{
|
||||
// Arrange
|
||||
var commandType = DynamicCommandBuilder.Compile(
|
||||
// lang=csharp
|
||||
"""
|
||||
[Command]
|
||||
public class Command : ICommand
|
||||
{
|
||||
[CommandOption("foo")]
|
||||
public string? Foo { get; init; }
|
||||
|
||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||
}
|
||||
"""
|
||||
);
|
||||
|
||||
var application = new CliApplicationBuilder()
|
||||
.AddCommand(commandType)
|
||||
.UseConsole(FakeConsole)
|
||||
.Build();
|
||||
|
||||
// Act
|
||||
var exitCode = await application.RunAsync(
|
||||
["--foo", "one", "two", "three"],
|
||||
new Dictionary<string, string>()
|
||||
);
|
||||
|
||||
// Assert
|
||||
exitCode.Should().NotBe(0);
|
||||
|
||||
var stdErr = FakeConsole.ReadErrorString();
|
||||
stdErr.Should().Contain("expects a single argument, but provided with multiple");
|
||||
}
|
||||
}
|
||||
263
CliFx.Tests/ParameterBindingSpecs.cs
Normal file
263
CliFx.Tests/ParameterBindingSpecs.cs
Normal file
@@ -0,0 +1,263 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using CliFx.Tests.Utils;
|
||||
using CliFx.Tests.Utils.Extensions;
|
||||
using FluentAssertions;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace CliFx.Tests;
|
||||
|
||||
public class ParameterBindingSpecs(ITestOutputHelper testOutput) : SpecsBase(testOutput)
|
||||
{
|
||||
[Fact]
|
||||
public async Task I_can_bind_a_parameter_to_a_property_and_get_the_value_from_the_corresponding_argument()
|
||||
{
|
||||
// Arrange
|
||||
var commandType = DynamicCommandBuilder.Compile(
|
||||
// lang=csharp
|
||||
"""
|
||||
[Command]
|
||||
public class Command : ICommand
|
||||
{
|
||||
[CommandParameter(0)]
|
||||
public required string Foo { get; init; }
|
||||
|
||||
[CommandParameter(1)]
|
||||
public required string Bar { get; init; }
|
||||
|
||||
public ValueTask ExecuteAsync(IConsole console)
|
||||
{
|
||||
console.WriteLine("Foo = " + Foo);
|
||||
console.WriteLine("Bar = " + Bar);
|
||||
|
||||
return default;
|
||||
}
|
||||
}
|
||||
"""
|
||||
);
|
||||
|
||||
var application = new CliApplicationBuilder()
|
||||
.AddCommand(commandType)
|
||||
.UseConsole(FakeConsole)
|
||||
.Build();
|
||||
|
||||
// Act
|
||||
var exitCode = await application.RunAsync(["one", "two"], new Dictionary<string, string>());
|
||||
|
||||
// Assert
|
||||
exitCode.Should().Be(0);
|
||||
|
||||
var stdOut = FakeConsole.ReadOutputString();
|
||||
stdOut.Should().ConsistOfLines("Foo = one", "Bar = two");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task I_can_bind_a_parameter_to_a_non_scalar_property_and_get_values_from_the_remaining_non_option_arguments()
|
||||
{
|
||||
// Arrange
|
||||
var commandType = DynamicCommandBuilder.Compile(
|
||||
// lang=csharp
|
||||
"""
|
||||
[Command]
|
||||
public class Command : ICommand
|
||||
{
|
||||
[CommandParameter(0)]
|
||||
public required string Foo { get; init; }
|
||||
|
||||
[CommandParameter(1)]
|
||||
public required string Bar { get; init; }
|
||||
|
||||
[CommandParameter(2)]
|
||||
public required IReadOnlyList<string> Baz { get; init; }
|
||||
|
||||
[CommandOption("boo")]
|
||||
public string? Boo { get; init; }
|
||||
|
||||
public ValueTask ExecuteAsync(IConsole console)
|
||||
{
|
||||
console.WriteLine("Foo = " + Foo);
|
||||
console.WriteLine("Bar = " + Bar);
|
||||
|
||||
foreach (var i in Baz)
|
||||
console.WriteLine("Baz = " + i);
|
||||
|
||||
return default;
|
||||
}
|
||||
}
|
||||
"""
|
||||
);
|
||||
|
||||
var application = new CliApplicationBuilder()
|
||||
.AddCommand(commandType)
|
||||
.UseConsole(FakeConsole)
|
||||
.Build();
|
||||
|
||||
// Act
|
||||
var exitCode = await application.RunAsync(
|
||||
["one", "two", "three", "four", "five", "--boo", "xxx"],
|
||||
new Dictionary<string, string>()
|
||||
);
|
||||
|
||||
// Assert
|
||||
exitCode.Should().Be(0);
|
||||
|
||||
var stdOut = FakeConsole.ReadOutputString();
|
||||
stdOut
|
||||
.Should()
|
||||
.ConsistOfLines("Foo = one", "Bar = two", "Baz = three", "Baz = four", "Baz = five");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task I_can_try_to_bind_a_parameter_to_a_property_and_get_an_error_if_the_user_does_not_provide_the_corresponding_argument()
|
||||
{
|
||||
// Arrange
|
||||
var commandType = DynamicCommandBuilder.Compile(
|
||||
// lang=csharp
|
||||
"""
|
||||
[Command]
|
||||
public class Command : ICommand
|
||||
{
|
||||
[CommandParameter(0)]
|
||||
public required string Foo { get; init; }
|
||||
|
||||
[CommandParameter(1)]
|
||||
public required string Bar { get; init; }
|
||||
|
||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||
}
|
||||
"""
|
||||
);
|
||||
|
||||
var application = new CliApplicationBuilder()
|
||||
.AddCommand(commandType)
|
||||
.UseConsole(FakeConsole)
|
||||
.Build();
|
||||
|
||||
// Act
|
||||
var exitCode = await application.RunAsync(["one"], new Dictionary<string, string>());
|
||||
|
||||
// Assert
|
||||
exitCode.Should().NotBe(0);
|
||||
|
||||
var stdErr = FakeConsole.ReadErrorString();
|
||||
stdErr.Should().Contain("Missing required parameter(s)");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task I_can_try_to_bind_a_parameter_to_a_non_scalar_property_and_get_an_error_if_the_user_does_not_provide_at_least_one_corresponding_argument()
|
||||
{
|
||||
// Arrange
|
||||
var commandType = DynamicCommandBuilder.Compile(
|
||||
// lang=csharp
|
||||
"""
|
||||
[Command]
|
||||
public class Command : ICommand
|
||||
{
|
||||
[CommandParameter(0)]
|
||||
public required string Foo { get; init; }
|
||||
|
||||
[CommandParameter(1)]
|
||||
public required IReadOnlyList<string> Bar { get; init; }
|
||||
|
||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||
}
|
||||
"""
|
||||
);
|
||||
|
||||
var application = new CliApplicationBuilder()
|
||||
.AddCommand(commandType)
|
||||
.UseConsole(FakeConsole)
|
||||
.Build();
|
||||
|
||||
// Act
|
||||
var exitCode = await application.RunAsync(["one"], new Dictionary<string, string>());
|
||||
|
||||
// Assert
|
||||
exitCode.Should().NotBe(0);
|
||||
|
||||
var stdErr = FakeConsole.ReadErrorString();
|
||||
stdErr.Should().Contain("Missing required parameter(s)");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task I_can_bind_a_non_required_parameter_to_a_property_and_get_no_value_if_the_user_does_not_provide_the_corresponding_argument()
|
||||
{
|
||||
// Arrange
|
||||
var commandType = DynamicCommandBuilder.Compile(
|
||||
// lang=csharp
|
||||
"""
|
||||
[Command]
|
||||
public class Command : ICommand
|
||||
{
|
||||
[CommandParameter(0)]
|
||||
public required string Foo { get; init; }
|
||||
|
||||
[CommandParameter(1, IsRequired = false)]
|
||||
public string? Bar { get; init; } = "xyz";
|
||||
|
||||
public ValueTask ExecuteAsync(IConsole console)
|
||||
{
|
||||
console.WriteLine("Foo = " + Foo);
|
||||
console.WriteLine("Bar = " + Bar);
|
||||
|
||||
return default;
|
||||
}
|
||||
}
|
||||
"""
|
||||
);
|
||||
|
||||
var application = new CliApplicationBuilder()
|
||||
.AddCommand(commandType)
|
||||
.UseConsole(FakeConsole)
|
||||
.Build();
|
||||
|
||||
// Act
|
||||
var exitCode = await application.RunAsync(["abc"], new Dictionary<string, string>());
|
||||
|
||||
// Assert
|
||||
exitCode.Should().Be(0);
|
||||
|
||||
var stdOut = FakeConsole.ReadOutputString();
|
||||
stdOut.Should().ConsistOfLines("Foo = abc", "Bar = xyz");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task I_can_try_to_bind_parameters_and_get_an_error_if_the_user_provides_too_many_arguments()
|
||||
{
|
||||
// Arrange
|
||||
var commandType = DynamicCommandBuilder.Compile(
|
||||
// lang=csharp
|
||||
"""
|
||||
[Command]
|
||||
public class Command : ICommand
|
||||
{
|
||||
[CommandParameter(0)]
|
||||
public required string Foo { get; init; }
|
||||
|
||||
[CommandParameter(1)]
|
||||
public required string Bar { get; init; }
|
||||
|
||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||
}
|
||||
"""
|
||||
);
|
||||
|
||||
var application = new CliApplicationBuilder()
|
||||
.AddCommand(commandType)
|
||||
.UseConsole(FakeConsole)
|
||||
.Build();
|
||||
|
||||
// Act
|
||||
var exitCode = await application.RunAsync(
|
||||
["one", "two", "three"],
|
||||
new Dictionary<string, string>()
|
||||
);
|
||||
|
||||
// Assert
|
||||
exitCode.Should().NotBe(0);
|
||||
|
||||
var stdErr = FakeConsole.ReadErrorString();
|
||||
stdErr.Should().Contain("Unexpected parameter(s)");
|
||||
}
|
||||
}
|
||||
179
CliFx.Tests/RoutingSpecs.cs
Normal file
179
CliFx.Tests/RoutingSpecs.cs
Normal file
@@ -0,0 +1,179 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using CliFx.Tests.Utils;
|
||||
using FluentAssertions;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace CliFx.Tests;
|
||||
|
||||
public class RoutingSpecs(ITestOutputHelper testOutput) : SpecsBase(testOutput)
|
||||
{
|
||||
[Fact]
|
||||
public async Task I_can_configure_a_command_to_be_executed_by_default_when_the_user_does_not_specify_a_command_name()
|
||||
{
|
||||
// Arrange
|
||||
var commandTypes = DynamicCommandBuilder.CompileMany(
|
||||
// lang=csharp
|
||||
"""
|
||||
[Command]
|
||||
public class DefaultCommand : ICommand
|
||||
{
|
||||
public ValueTask ExecuteAsync(IConsole console)
|
||||
{
|
||||
console.WriteLine("default");
|
||||
return default;
|
||||
}
|
||||
}
|
||||
|
||||
[Command("cmd")]
|
||||
public class NamedCommand : ICommand
|
||||
{
|
||||
public ValueTask ExecuteAsync(IConsole console)
|
||||
{
|
||||
console.WriteLine("cmd");
|
||||
return default;
|
||||
}
|
||||
}
|
||||
|
||||
[Command("cmd child")]
|
||||
public class NamedChildCommand : ICommand
|
||||
{
|
||||
public ValueTask ExecuteAsync(IConsole console)
|
||||
{
|
||||
console.WriteLine("cmd child");
|
||||
return default;
|
||||
}
|
||||
}
|
||||
"""
|
||||
);
|
||||
|
||||
var application = new CliApplicationBuilder()
|
||||
.AddCommands(commandTypes)
|
||||
.UseConsole(FakeConsole)
|
||||
.Build();
|
||||
|
||||
// Act
|
||||
var exitCode = await application.RunAsync(
|
||||
[],
|
||||
new Dictionary<string, string>()
|
||||
);
|
||||
|
||||
// Assert
|
||||
exitCode.Should().Be(0);
|
||||
|
||||
var stdOut = FakeConsole.ReadOutputString();
|
||||
stdOut.Trim().Should().Be("default");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task I_can_configure_a_command_to_be_executed_when_the_user_specifies_its_name()
|
||||
{
|
||||
// Arrange
|
||||
var commandTypes = DynamicCommandBuilder.CompileMany(
|
||||
// lang=csharp
|
||||
"""
|
||||
[Command]
|
||||
public class DefaultCommand : ICommand
|
||||
{
|
||||
public ValueTask ExecuteAsync(IConsole console)
|
||||
{
|
||||
console.WriteLine("default");
|
||||
return default;
|
||||
}
|
||||
}
|
||||
|
||||
[Command("cmd")]
|
||||
public class NamedCommand : ICommand
|
||||
{
|
||||
public ValueTask ExecuteAsync(IConsole console)
|
||||
{
|
||||
console.WriteLine("cmd");
|
||||
return default;
|
||||
}
|
||||
}
|
||||
|
||||
[Command("cmd child")]
|
||||
public class NamedChildCommand : ICommand
|
||||
{
|
||||
public ValueTask ExecuteAsync(IConsole console)
|
||||
{
|
||||
console.WriteLine("cmd child");
|
||||
return default;
|
||||
}
|
||||
}
|
||||
"""
|
||||
);
|
||||
|
||||
var application = new CliApplicationBuilder()
|
||||
.AddCommands(commandTypes)
|
||||
.UseConsole(FakeConsole)
|
||||
.Build();
|
||||
|
||||
// Act
|
||||
var exitCode = await application.RunAsync(["cmd"], new Dictionary<string, string>());
|
||||
|
||||
// Assert
|
||||
exitCode.Should().Be(0);
|
||||
|
||||
var stdOut = FakeConsole.ReadOutputString();
|
||||
stdOut.Trim().Should().Be("cmd");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task I_can_configure_a_nested_command_to_be_executed_when_the_user_specifies_its_name()
|
||||
{
|
||||
// Arrange
|
||||
var commandTypes = DynamicCommandBuilder.CompileMany(
|
||||
// lang=csharp
|
||||
"""
|
||||
[Command]
|
||||
public class DefaultCommand : ICommand
|
||||
{
|
||||
public ValueTask ExecuteAsync(IConsole console)
|
||||
{
|
||||
console.WriteLine("default");
|
||||
return default;
|
||||
}
|
||||
}
|
||||
|
||||
[Command("cmd")]
|
||||
public class NamedCommand : ICommand
|
||||
{
|
||||
public ValueTask ExecuteAsync(IConsole console)
|
||||
{
|
||||
console.WriteLine("cmd");
|
||||
return default;
|
||||
}
|
||||
}
|
||||
|
||||
[Command("cmd child")]
|
||||
public class NamedChildCommand : ICommand
|
||||
{
|
||||
public ValueTask ExecuteAsync(IConsole console)
|
||||
{
|
||||
console.WriteLine("cmd child");
|
||||
return default;
|
||||
}
|
||||
}
|
||||
"""
|
||||
);
|
||||
|
||||
var application = new CliApplicationBuilder()
|
||||
.AddCommands(commandTypes)
|
||||
.UseConsole(FakeConsole)
|
||||
.Build();
|
||||
|
||||
// Act
|
||||
var exitCode = await application.RunAsync(
|
||||
["cmd", "child"],
|
||||
new Dictionary<string, string>()
|
||||
);
|
||||
|
||||
// Assert
|
||||
exitCode.Should().Be(0);
|
||||
|
||||
var stdOut = FakeConsole.ReadOutputString();
|
||||
stdOut.Trim().Should().Be("cmd child");
|
||||
}
|
||||
}
|
||||
19
CliFx.Tests/SpecsBase.cs
Normal file
19
CliFx.Tests/SpecsBase.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
using System;
|
||||
using CliFx.Infrastructure;
|
||||
using CliFx.Tests.Utils.Extensions;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace CliFx.Tests;
|
||||
|
||||
public abstract class SpecsBase(ITestOutputHelper testOutput) : IDisposable
|
||||
{
|
||||
public ITestOutputHelper TestOutput { get; } = testOutput;
|
||||
|
||||
public FakeInMemoryConsole FakeConsole { get; } = new();
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
FakeConsole.DumpToTestOutput(TestOutput);
|
||||
FakeConsole.Dispose();
|
||||
}
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
using System;
|
||||
using FluentAssertions;
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace CliFx.Tests
|
||||
{
|
||||
[TestFixture]
|
||||
public class SystemConsoleTests
|
||||
{
|
||||
[TearDown]
|
||||
public void TearDown()
|
||||
{
|
||||
// Reset console color so it doesn't carry on into the next tests
|
||||
Console.ResetColor();
|
||||
}
|
||||
|
||||
[Test(Description = "Must be in sync with system console")]
|
||||
public void Smoke_Test()
|
||||
{
|
||||
// Arrange
|
||||
var console = new SystemConsole();
|
||||
|
||||
// Act
|
||||
console.ResetColor();
|
||||
console.ForegroundColor = ConsoleColor.DarkMagenta;
|
||||
console.BackgroundColor = ConsoleColor.DarkMagenta;
|
||||
|
||||
// Assert
|
||||
console.Input.Should().BeSameAs(Console.In);
|
||||
console.IsInputRedirected.Should().Be(Console.IsInputRedirected);
|
||||
console.Output.Should().BeSameAs(Console.Out);
|
||||
console.IsOutputRedirected.Should().Be(Console.IsOutputRedirected);
|
||||
console.Error.Should().BeSameAs(Console.Error);
|
||||
console.IsErrorRedirected.Should().Be(Console.IsErrorRedirected);
|
||||
console.ForegroundColor.Should().Be(Console.ForegroundColor);
|
||||
console.BackgroundColor.Should().Be(Console.BackgroundColor);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
using System.Threading.Tasks;
|
||||
using CliFx.Attributes;
|
||||
|
||||
namespace CliFx.Tests.TestCommands
|
||||
{
|
||||
[Command("allrequired", Description = "AllRequiredOptionsCommand description.")]
|
||||
public class AllRequiredOptionsCommand : ICommand
|
||||
{
|
||||
[CommandOption("option-f", 'f', IsRequired = true, Description = "OptionF description.")]
|
||||
public string? OptionF { get; set; }
|
||||
|
||||
[CommandOption("option-g", 'g', IsRequired = true, Description = "OptionG description.")]
|
||||
public string? OptionFG { get; set; }
|
||||
|
||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||
}
|
||||
}
|
||||
@@ -1,126 +0,0 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using CliFx.Attributes;
|
||||
using CliFx.Tests.TestCustomTypes;
|
||||
|
||||
namespace CliFx.Tests.TestCommands
|
||||
{
|
||||
[Command]
|
||||
public class AllSupportedTypesCommand : ICommand
|
||||
{
|
||||
[CommandOption(nameof(Object))]
|
||||
public object? Object { get; set; } = 42;
|
||||
|
||||
[CommandOption(nameof(String))]
|
||||
public string? String { get; set; } = "foo bar";
|
||||
|
||||
[CommandOption(nameof(Bool))]
|
||||
public bool Bool { get; set; }
|
||||
|
||||
[CommandOption(nameof(Char))]
|
||||
public char Char { get; set; }
|
||||
|
||||
[CommandOption(nameof(Sbyte))]
|
||||
public sbyte Sbyte { get; set; }
|
||||
|
||||
[CommandOption(nameof(Byte))]
|
||||
public byte Byte { get; set; }
|
||||
|
||||
[CommandOption(nameof(Short))]
|
||||
public short Short { get; set; }
|
||||
|
||||
[CommandOption(nameof(Ushort))]
|
||||
public ushort Ushort { get; set; }
|
||||
|
||||
[CommandOption(nameof(Int))]
|
||||
public int Int { get; set; }
|
||||
|
||||
[CommandOption(nameof(Uint))]
|
||||
public uint Uint { get; set; }
|
||||
|
||||
[CommandOption(nameof(Long))]
|
||||
public long Long { get; set; }
|
||||
|
||||
[CommandOption(nameof(Ulong))]
|
||||
public ulong Ulong { get; set; }
|
||||
|
||||
[CommandOption(nameof(Float))]
|
||||
public float Float { get; set; }
|
||||
|
||||
[CommandOption(nameof(Double))]
|
||||
public double Double { get; set; }
|
||||
|
||||
[CommandOption(nameof(Decimal))]
|
||||
public decimal Decimal { get; set; }
|
||||
|
||||
[CommandOption(nameof(DateTime))]
|
||||
public DateTime DateTime { get; set; }
|
||||
|
||||
[CommandOption(nameof(DateTimeOffset))]
|
||||
public DateTimeOffset DateTimeOffset { get; set; }
|
||||
|
||||
[CommandOption(nameof(TimeSpan))]
|
||||
public TimeSpan TimeSpan { get; set; }
|
||||
|
||||
[CommandOption(nameof(TestEnum))]
|
||||
public TestEnum TestEnum { get; set; }
|
||||
|
||||
[CommandOption(nameof(IntNullable))]
|
||||
public int? IntNullable { get; set; }
|
||||
|
||||
[CommandOption(nameof(TestEnumNullable))]
|
||||
public TestEnum? TestEnumNullable { get; set; }
|
||||
|
||||
[CommandOption(nameof(TimeSpanNullable))]
|
||||
public TimeSpan? TimeSpanNullable { get; set; }
|
||||
|
||||
[CommandOption(nameof(TestStringConstructable))]
|
||||
public TestStringConstructable? TestStringConstructable { get; set; }
|
||||
|
||||
[CommandOption(nameof(TestStringParseable))]
|
||||
public TestStringParseable? TestStringParseable { get; set; }
|
||||
|
||||
[CommandOption(nameof(TestStringParseableWithFormatProvider))]
|
||||
public TestStringParseableWithFormatProvider? TestStringParseableWithFormatProvider { get; set; }
|
||||
|
||||
[CommandOption(nameof(ObjectArray))]
|
||||
public object[]? ObjectArray { get; set; }
|
||||
|
||||
[CommandOption(nameof(StringArray))]
|
||||
public string[]? StringArray { get; set; }
|
||||
|
||||
[CommandOption(nameof(IntArray))]
|
||||
public int[]? IntArray { get; set; }
|
||||
|
||||
[CommandOption(nameof(TestEnumArray))]
|
||||
public TestEnum[]? TestEnumArray { get; set; }
|
||||
|
||||
[CommandOption(nameof(IntNullableArray))]
|
||||
public int?[]? IntNullableArray { get; set; }
|
||||
|
||||
[CommandOption(nameof(TestStringConstructableArray))]
|
||||
public TestStringConstructable[]? TestStringConstructableArray { get; set; }
|
||||
|
||||
[CommandOption(nameof(Enumerable))]
|
||||
public IEnumerable? Enumerable { get; set; }
|
||||
|
||||
[CommandOption(nameof(StringEnumerable))]
|
||||
public IEnumerable<string>? StringEnumerable { get; set; }
|
||||
|
||||
[CommandOption(nameof(StringReadOnlyList))]
|
||||
public IReadOnlyList<string>? StringReadOnlyList { get; set; }
|
||||
|
||||
[CommandOption(nameof(StringList))]
|
||||
public List<string>? StringList { get; set; }
|
||||
|
||||
[CommandOption(nameof(StringHashSet))]
|
||||
public HashSet<string>? StringHashSet { get; set; }
|
||||
|
||||
[CommandOption(nameof(NonConvertible))]
|
||||
public TestNonStringParseable? NonConvertible { get; set; }
|
||||
|
||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
using System.Threading.Tasks;
|
||||
using CliFx.Attributes;
|
||||
using CliFx.Tests.TestCustomTypes;
|
||||
|
||||
namespace CliFx.Tests.TestCommands
|
||||
{
|
||||
[Command]
|
||||
public class BrokenEnumerableCommand : ICommand
|
||||
{
|
||||
[CommandParameter(0)]
|
||||
public TestCustomEnumerable<string>? Test { get; set; }
|
||||
|
||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using CliFx.Attributes;
|
||||
|
||||
namespace CliFx.Tests.TestCommands
|
||||
{
|
||||
[Command("cancel")]
|
||||
public class CancellableCommand : ICommand
|
||||
{
|
||||
public async ValueTask ExecuteAsync(IConsole console)
|
||||
{
|
||||
await Task.Delay(TimeSpan.FromSeconds(3), console.GetCancellationToken());
|
||||
console.Output.WriteLine("Never printed");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
using System.Threading.Tasks;
|
||||
using CliFx.Attributes;
|
||||
using CliFx.Exceptions;
|
||||
|
||||
namespace CliFx.Tests.TestCommands
|
||||
{
|
||||
[Command("exc")]
|
||||
public class CommandExceptionCommand : ICommand
|
||||
{
|
||||
[CommandOption("code", 'c')]
|
||||
public int ExitCode { get; set; } = 1337;
|
||||
|
||||
[CommandOption("msg", 'm')]
|
||||
public string? Message { get; set; }
|
||||
|
||||
public ValueTask ExecuteAsync(IConsole console) => throw new CommandException(Message, ExitCode);
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using CliFx.Attributes;
|
||||
|
||||
namespace CliFx.Tests.TestCommands
|
||||
{
|
||||
[Command("concat", Description = "Concatenate strings.")]
|
||||
public class ConcatCommand : ICommand
|
||||
{
|
||||
[CommandOption('i', IsRequired = true, Description = "Input strings.")]
|
||||
public IReadOnlyList<string> Inputs { get; set; }
|
||||
|
||||
[CommandOption('s', Description = "String separator.")]
|
||||
public string Separator { get; set; } = "";
|
||||
|
||||
public ValueTask ExecuteAsync(IConsole console)
|
||||
{
|
||||
console.Output.WriteLine(string.Join(Separator, Inputs));
|
||||
return default;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
using System.Threading.Tasks;
|
||||
using CliFx.Attributes;
|
||||
|
||||
namespace CliFx.Tests.TestCommands
|
||||
{
|
||||
[Command("div", Description = "Divide one number by another.")]
|
||||
public class DivideCommand : ICommand
|
||||
{
|
||||
[CommandOption("dividend", 'D', IsRequired = true, Description = "The number to divide.")]
|
||||
public double Dividend { get; set; }
|
||||
|
||||
[CommandOption("divisor", 'd', IsRequired = true, Description = "The number to divide by.")]
|
||||
public double Divisor { get; set; }
|
||||
|
||||
// This property should be ignored by resolver
|
||||
public bool NotAnOption { get; set; }
|
||||
|
||||
public ValueTask ExecuteAsync(IConsole console)
|
||||
{
|
||||
console.Output.WriteLine(Dividend / Divisor);
|
||||
return default;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
using System.Threading.Tasks;
|
||||
using CliFx.Attributes;
|
||||
|
||||
namespace CliFx.Tests.TestCommands
|
||||
{
|
||||
[Command]
|
||||
public class DuplicateOptionEnvironmentVariableNamesCommand : ICommand
|
||||
{
|
||||
[CommandOption("option-a", EnvironmentVariableName = "ENV_VAR")]
|
||||
public string? OptionA { get; set; }
|
||||
|
||||
[CommandOption("option-b", EnvironmentVariableName = "ENV_VAR")]
|
||||
public string? OptionB { get; set; }
|
||||
|
||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
using System.Threading.Tasks;
|
||||
using CliFx.Attributes;
|
||||
|
||||
namespace CliFx.Tests.TestCommands
|
||||
{
|
||||
[Command]
|
||||
public class DuplicateOptionNamesCommand : ICommand
|
||||
{
|
||||
[CommandOption("fruits")]
|
||||
public string? Apples { get; set; }
|
||||
|
||||
[CommandOption("fruits")]
|
||||
public string? Oranges { get; set; }
|
||||
|
||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
using System.Threading.Tasks;
|
||||
using CliFx.Attributes;
|
||||
|
||||
namespace CliFx.Tests.TestCommands
|
||||
{
|
||||
[Command]
|
||||
public class DuplicateOptionShortNamesCommand : ICommand
|
||||
{
|
||||
[CommandOption('x')]
|
||||
public string? OptionA { get; set; }
|
||||
|
||||
[CommandOption('x')]
|
||||
public string? OptionB { get; set; }
|
||||
|
||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
using System.Threading.Tasks;
|
||||
using CliFx.Attributes;
|
||||
|
||||
namespace CliFx.Tests.TestCommands
|
||||
{
|
||||
[Command]
|
||||
public class DuplicateParameterNameCommand : ICommand
|
||||
{
|
||||
[CommandParameter(0, Name = "param")]
|
||||
public string? ParameterA { get; set; }
|
||||
|
||||
[CommandParameter(1, Name = "param")]
|
||||
public string? ParameterB { get; set; }
|
||||
|
||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user