mirror of
				https://github.com/Tyrrrz/CliFx.git
				synced 2025-10-25 15:19:17 +00:00 
			
		
		
		
	Compare commits
	
		
			412 Commits
		
	
	
		
			0.0.6
			...
			432c8a66af
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | 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 | ||
|  | 4365ad457a | ||
|  | fb3617980e | ||
|  | 7690aae456 | ||
|  | 076678a08c | ||
|  | 104279d6e9 | ||
|  | 515d51a91d | ||
|  | 4fdf543190 | ||
|  | 4e1ab096c9 | ||
|  | 8aa6911cca | ||
|  | f0362019ed | ||
|  | 82895f2e42 | ||
|  | 4cf622abe5 | ||
|  | d4e22a78d6 | ||
|  | 3883c831e9 | ||
|  | 63441688fe | ||
|  | e48839b938 | ||
|  | ed87373dc3 | ||
|  | 6ce52c70f7 | ||
|  | d2b0b16121 | ||
|  | d67a9fe762 | ||
|  | ce2a3153e6 | ||
|  | d4b54231fb | ||
|  | 70bfe0bf91 | ||
|  | 9690c380d3 | ||
|  | 85caa275ae | ||
|  | 32026e59c0 | ||
|  | 486ccb9685 | ||
|  | 7b766f70f3 | ||
|  | f73e96488f | ||
|  | af63fa5a1f | ||
|  | e8f53c9463 | 
							
								
								
									
										
											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 | ||||
							
								
								
									
										4
									
								
								.github/FUNDING.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.github/FUNDING.yml
									
									
									
									
										vendored
									
									
								
							| @@ -1,4 +0,0 @@ | ||||
| github: Tyrrrz | ||||
| patreon: Tyrrrz | ||||
| open_collective: Tyrrrz | ||||
| custom: ['buymeacoffee.com/Tyrrrz'] | ||||
							
								
								
									
										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: | ||||
|           - "*" | ||||
							
								
								
									
										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: 23 KiB | 
| @@ -1,35 +0,0 @@ | ||||
| using System.Threading.Tasks; | ||||
| using BenchmarkDotNet.Attributes; | ||||
| using CliFx.Benchmarks.Commands; | ||||
|  | ||||
| namespace CliFx.Benchmarks | ||||
| { | ||||
|     [CoreJob] | ||||
|     [RankColumn] | ||||
|     public class Benchmark | ||||
|     { | ||||
|         private static readonly string[] Arguments = {"--str", "hello world", "-i", "13", "-b"}; | ||||
|  | ||||
|         [Benchmark(Description = "CliFx", Baseline = true)] | ||||
|         public Task<int> ExecuteWithCliFx() => new CliApplicationBuilder().AddCommand(typeof(CliFxCommand)).Build().RunAsync(Arguments); | ||||
|  | ||||
|         [Benchmark(Description = "System.CommandLine")] | ||||
|         public Task<int> ExecuteWithSystemCommandLine() => 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() | ||||
|         { | ||||
|             var parsed = new CommandLine.Parser().ParseArguments(Arguments, typeof(CommandLineParserCommand)); | ||||
|             CommandLine.ParserResultExtensions.WithParsed<CommandLineParserCommand>(parsed, 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(); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										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>()); | ||||
| } | ||||
							
								
								
									
										24
									
								
								CliFx.Benchmarks/Benchmarks.Clipr.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								CliFx.Benchmarks/Benchmarks.Clipr.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,24 @@ | ||||
| using BenchmarkDotNet.Attributes; | ||||
| using clipr; | ||||
|  | ||||
| namespace CliFx.Benchmarks; | ||||
|  | ||||
| public partial class Benchmarks | ||||
| { | ||||
|     public class CliprCommand | ||||
|     { | ||||
|         [NamedArgument('s', "str")] | ||||
|         public string? StrOption { get; set; } | ||||
|  | ||||
|         [NamedArgument('i', "int")] | ||||
|         public int IntOption { get; set; } | ||||
|  | ||||
|         [NamedArgument('b', "bool", Constraint = NumArgsConstraint.Optional, Const = true)] | ||||
|         public bool BoolOption { get; set; } | ||||
|  | ||||
|         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); | ||||
| } | ||||
							
								
								
									
										24
									
								
								CliFx.Benchmarks/Benchmarks.PowerArgs.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								CliFx.Benchmarks/Benchmarks.PowerArgs.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,24 @@ | ||||
| using BenchmarkDotNet.Attributes; | ||||
| using PowerArgs; | ||||
|  | ||||
| namespace CliFx.Benchmarks; | ||||
|  | ||||
| public partial class Benchmarks | ||||
| { | ||||
|     public class PowerArgsCommand | ||||
|     { | ||||
|         [ArgShortcut("--str"), ArgShortcut("-s")] | ||||
|         public string? StrOption { get; set; } | ||||
|  | ||||
|         [ArgShortcut("--int"), ArgShortcut("-i")] | ||||
|         public int IntOption { get; set; } | ||||
|  | ||||
|         [ArgShortcut("--bool"), ArgShortcut("-b")] | ||||
|         public bool BoolOption { get; set; } | ||||
|  | ||||
|         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) | ||||
|         ); | ||||
| } | ||||
| @@ -2,16 +2,19 @@ | ||||
|  | ||||
|   <PropertyGroup> | ||||
|     <OutputType>Exe</OutputType> | ||||
|     <TargetFramework>netcoreapp3.0</TargetFramework> | ||||
|     <TargetFramework>net9.0</TargetFramework> | ||||
|     <NuGetAudit>false</NuGetAudit> | ||||
|   </PropertyGroup> | ||||
|  | ||||
|   <ItemGroup> | ||||
|     <PackageReference Include="BenchmarkDotNet" Version="0.11.5" /> | ||||
|     <PackageReference Include="BenchmarkDotNet" Version="0.14.0" /> | ||||
|     <PackageReference Include="clipr" Version="1.6.1" /> | ||||
|     <PackageReference Include="CommandLineParser" Version="2.6.0" /> | ||||
|     <PackageReference Include="McMaster.Extensions.CommandLineUtils" Version="2.3.4" /> | ||||
|     <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="0.30.6" 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-beta4.22272.1" /> | ||||
|   </ItemGroup> | ||||
|  | ||||
|   <ItemGroup> | ||||
|   | ||||
| @@ -1,22 +0,0 @@ | ||||
| using System.Threading; | ||||
| using System.Threading.Tasks; | ||||
| using CliFx.Attributes; | ||||
| using CliFx.Services; | ||||
|  | ||||
| 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 Task ExecuteAsync(IConsole console, CancellationToken cancellationToken) => Task.CompletedTask; | ||||
|     } | ||||
| } | ||||
| @@ -1,20 +0,0 @@ | ||||
| using clipr; | ||||
|  | ||||
| namespace CliFx.Benchmarks.Commands | ||||
| { | ||||
|     public class CliprCommand | ||||
|     { | ||||
|         [NamedArgument('s', "str")] | ||||
|         public string StrOption { get; set; } | ||||
|  | ||||
|         [NamedArgument('i', "int")] | ||||
|         public int IntOption { get; set; } | ||||
|  | ||||
|         [NamedArgument('b', "bool", Constraint = NumArgsConstraint.Optional, Const = true)] | ||||
|         public bool BoolOption { get; set; } | ||||
|  | ||||
|         public void Execute() | ||||
|         { | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -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,20 +0,0 @@ | ||||
| using PowerArgs; | ||||
|  | ||||
| namespace CliFx.Benchmarks.Commands | ||||
| { | ||||
|     public class PowerArgsCommand | ||||
|     { | ||||
|         [ArgShortcut("--str"), ArgShortcut("-s")] | ||||
|         public string StrOption { get; set; } | ||||
|  | ||||
|         [ArgShortcut("--int"), ArgShortcut("-i")] | ||||
|         public int IntOption { get; set; } | ||||
|  | ||||
|         [ArgShortcut("--bool"), ArgShortcut("-b")] | ||||
|         public bool BoolOption { get; set; } | ||||
|  | ||||
|         public void Main() | ||||
|         { | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -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 | | ||||
| @@ -2,16 +2,19 @@ | ||||
|  | ||||
|   <PropertyGroup> | ||||
|     <OutputType>Exe</OutputType> | ||||
|     <TargetFramework>netcoreapp3.0</TargetFramework> | ||||
|     <TargetFramework>net9.0</TargetFramework> | ||||
|     <ApplicationIcon>../favicon.ico</ApplicationIcon> | ||||
|     <PublishAot>true</PublishAot> | ||||
|   </PropertyGroup> | ||||
|  | ||||
|   <ItemGroup> | ||||
|     <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="2.2.0" /> | ||||
|     <PackageReference Include="Newtonsoft.Json" Version="12.0.2" /> | ||||
|     <PackageReference Include="CSharpier.MsBuild" Version="0.30.6" PrivateAssets="all" /> | ||||
|     <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.1" /> | ||||
|   </ItemGroup> | ||||
|  | ||||
|   <ItemGroup> | ||||
|     <ProjectReference Include="..\CliFx\CliFx.csproj" /> | ||||
|     <ProjectReference Include="..\CliFx.SourceGeneration\CliFx.SourceGeneration.csproj" ReferenceOutputAssembly="false" OutputItemType="analyzer" /> | ||||
|   </ItemGroup> | ||||
|  | ||||
| </Project> | ||||
| @@ -1,76 +1,55 @@ | ||||
| using System; | ||||
| using System.Threading; | ||||
| 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.Services; | ||||
| 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); | ||||
|  | ||||
|         [CommandOption("title", 't', IsRequired = true, 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; } | ||||
|  | ||||
|         [CommandOption("isbn", 'n', Description = "Book ISBN.")] | ||||
|         public Isbn Isbn { get; set; } | ||||
|  | ||||
|         public BookAddCommand(LibraryService libraryService) | ||||
|         { | ||||
|             _libraryService = libraryService; | ||||
|         } | ||||
|  | ||||
|         public Task ExecuteAsync(IConsole console, CancellationToken cancellationToken) | ||||
|         { | ||||
|             // To make the demo simpler, we will just generate random publish date and ISBN if they were not set | ||||
|             if (Published == default) | ||||
|                 Published = CreateRandomDate(); | ||||
|             if (Isbn == default) | ||||
|                 Isbn = CreateRandomIsbn(); | ||||
|  | ||||
|             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 Task.CompletedTask; | ||||
|         } | ||||
|         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); | ||||
|  | ||||
|         public 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,36 +1,27 @@ | ||||
| using System.Threading; | ||||
| using System.Threading.Tasks; | ||||
| 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.Services; | ||||
| 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); | ||||
|  | ||||
|         [CommandOption("title", 't', IsRequired = true, 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 Task ExecuteAsync(IConsole console, CancellationToken cancellationToken) | ||||
|         { | ||||
|             var book = _libraryService.GetBook(Title); | ||||
|  | ||||
|             if (book == null) | ||||
|                 throw new CommandException("Book not found.", 1); | ||||
|  | ||||
|             console.RenderBook(book); | ||||
|  | ||||
|             return Task.CompletedTask; | ||||
|         } | ||||
|         return default; | ||||
|     } | ||||
| } | ||||
| } | ||||
|   | ||||
| @@ -1,39 +1,29 @@ | ||||
| using System.Threading; | ||||
| using System.Threading.Tasks; | ||||
| using System.Threading.Tasks; | ||||
| using CliFx.Attributes; | ||||
| using CliFx.Demo.Internal; | ||||
| using CliFx.Demo.Services; | ||||
| using CliFx.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 Task ExecuteAsync(IConsole console, CancellationToken cancellationToken) | ||||
|         { | ||||
|             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 Task.CompletedTask; | ||||
|         } | ||||
|         return default; | ||||
|     } | ||||
| } | ||||
| } | ||||
|   | ||||
| @@ -1,37 +1,28 @@ | ||||
| using System.Threading; | ||||
| using System.Threading.Tasks; | ||||
| using System.Threading.Tasks; | ||||
| using CliFx.Attributes; | ||||
| using CliFx.Demo.Services; | ||||
| using CliFx.Demo.Domain; | ||||
| using CliFx.Exceptions; | ||||
| using CliFx.Services; | ||||
| 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); | ||||
|  | ||||
|         [CommandOption("title", 't', IsRequired = true, 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 Task ExecuteAsync(IConsole console, CancellationToken cancellationToken) | ||||
|         { | ||||
|             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 Task.CompletedTask; | ||||
|         } | ||||
|         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,30 +0,0 @@ | ||||
| using System; | ||||
| using CliFx.Demo.Models; | ||||
| using CliFx.Services; | ||||
|  | ||||
| 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,44 +0,0 @@ | ||||
| using System; | ||||
| using System.Globalization; | ||||
|  | ||||
| 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) | ||||
|         { | ||||
|             var components = value.Split('-', 5, StringSplitOptions.RemoveEmptyEntries); | ||||
|  | ||||
|             return new Isbn( | ||||
|                 int.Parse(components[0], CultureInfo.InvariantCulture), | ||||
|                 int.Parse(components[1], CultureInfo.InvariantCulture), | ||||
|                 int.Parse(components[2], CultureInfo.InvariantCulture), | ||||
|                 int.Parse(components[3], CultureInfo.InvariantCulture), | ||||
|                 int.Parse(components[4], CultureInfo.InvariantCulture)); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -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,33 +1,18 @@ | ||||
| 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 | ||||
|     { | ||||
|         public static Task<int> Main(string[] args) | ||||
|         { | ||||
|             // 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>(); | ||||
|  | ||||
|             var serviceProvider = services.BuildServiceProvider(); | ||||
|  | ||||
|             return new CliApplicationBuilder() | ||||
|                 .AddCommandsFromThisAssembly() | ||||
|                 .UseCommandFactory(schema => (ICommand) serviceProvider.GetRequiredService(schema.Type)) | ||||
|                 .Build() | ||||
|                 .RunAsync(args); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 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, option 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="0.28.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.12.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() | ||||
|         ); | ||||
| } | ||||
							
								
								
									
										167
									
								
								CliFx.SourceGeneration/SemanticModel/CommandSymbol.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										167
									
								
								CliFx.SourceGeneration/SemanticModel/CommandSymbol.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,167 @@ | ||||
| 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); | ||||
| } | ||||
							
								
								
									
										18
									
								
								CliFx.Tests.Dummy/CliFx.Tests.Dummy.csproj
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								CliFx.Tests.Dummy/CliFx.Tests.Dummy.csproj
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,18 @@ | ||||
| <Project Sdk="Microsoft.NET.Sdk"> | ||||
|  | ||||
|   <PropertyGroup> | ||||
|     <OutputType>Exe</OutputType> | ||||
|     <TargetFramework>net9.0</TargetFramework> | ||||
|     <ApplicationIcon>../favicon.ico</ApplicationIcon> | ||||
|   </PropertyGroup> | ||||
|  | ||||
|   <ItemGroup> | ||||
|     <PackageReference Include="CSharpier.MsBuild" Version="0.30.6" 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; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										28
									
								
								CliFx.Tests.Dummy/Program.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								CliFx.Tests.Dummy/Program.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,28 @@ | ||||
| using System; | ||||
| using System.IO; | ||||
| using System.Reflection; | ||||
| using System.Threading.Tasks; | ||||
|  | ||||
| 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 | ||||
| { | ||||
|     // Path to the apphost | ||||
|     public static string FilePath { get; } = | ||||
|         Path.ChangeExtension( | ||||
|             Assembly.GetExecutingAssembly().Location, | ||||
|             OperatingSystem.IsWindows() ? "exe" : null | ||||
|         ); | ||||
|  | ||||
|     public static async Task Main() | ||||
|     { | ||||
|         // 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,51 +0,0 @@ | ||||
| using NUnit.Framework; | ||||
| using System; | ||||
| using System.IO; | ||||
| using CliFx.Services; | ||||
| using CliFx.Tests.Stubs; | ||||
| using CliFx.Tests.TestCommands; | ||||
|  | ||||
| namespace CliFx.Tests | ||||
| { | ||||
|     [TestFixture] | ||||
|     public class CliApplicationBuilderTests | ||||
|     { | ||||
|         // Make sure all builder methods work | ||||
|         [Test] | ||||
|         public void All_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)) | ||||
|                 .UseCommandFactory(schema => (ICommand)Activator.CreateInstance(schema.Type)) | ||||
|                 .UseCommandOptionInputConverter(new CommandOptionInputConverter()) | ||||
|                 .UseEnvironmentVariablesProvider(new EnvironmentVariablesProviderStub()) | ||||
|                 .Build(); | ||||
|         } | ||||
|  | ||||
|         // Make sure builder can produce an application with no parameters specified | ||||
|         [Test] | ||||
|         public void Build_Test() | ||||
|         { | ||||
|             // Arrange | ||||
|             var builder = new CliApplicationBuilder(); | ||||
|  | ||||
|             // Act | ||||
|             builder.Build(); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -1,261 +0,0 @@ | ||||
| using FluentAssertions; | ||||
| using NUnit.Framework; | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
| using System.IO; | ||||
| using System.Threading.Tasks; | ||||
| using CliFx.Services; | ||||
| using CliFx.Tests.Stubs; | ||||
| using CliFx.Tests.TestCommands; | ||||
|  | ||||
| namespace CliFx.Tests | ||||
| { | ||||
|     [TestFixture] | ||||
|     public class CliApplicationTests | ||||
|     { | ||||
|         private const string TestVersionText = "v1.0"; | ||||
|  | ||||
|         private static IEnumerable<TestCaseData> GetTestCases_RunAsync() | ||||
|         { | ||||
|             yield return new TestCaseData( | ||||
|                 new[] { typeof(HelloWorldDefaultCommand) }, | ||||
|                 new string[0], | ||||
|                 "Hello world." | ||||
|             ); | ||||
|  | ||||
|             yield return new TestCaseData( | ||||
|                 new[] { typeof(ConcatCommand) }, | ||||
|                 new[] { "concat", "-i", "foo", "-i", "bar", "-s", " " }, | ||||
|                 "foo bar" | ||||
|             ); | ||||
|  | ||||
|             yield return new TestCaseData( | ||||
|                 new[] { typeof(ConcatCommand) }, | ||||
|                 new[] { "concat", "-i", "one", "two", "three", "-s", ", " }, | ||||
|                 "one, two, three" | ||||
|             ); | ||||
|  | ||||
|             yield return new TestCaseData( | ||||
|                 new[] { typeof(DivideCommand) }, | ||||
|                 new[] { "div", "-D", "24", "-d", "8" }, | ||||
|                 "3" | ||||
|             ); | ||||
|  | ||||
|             yield return new TestCaseData( | ||||
|                 new[] { typeof(HelloWorldDefaultCommand) }, | ||||
|                 new[] { "--version" }, | ||||
|                 TestVersionText | ||||
|             ); | ||||
|  | ||||
|             yield return new TestCaseData( | ||||
|                 new[] { typeof(ConcatCommand) }, | ||||
|                 new[] { "--version" }, | ||||
|                 TestVersionText | ||||
|             ); | ||||
|  | ||||
|             yield return new TestCaseData( | ||||
|                 new[] { typeof(HelloWorldDefaultCommand) }, | ||||
|                 new[] { "-h" }, | ||||
|                 null | ||||
|             ); | ||||
|  | ||||
|             yield return new TestCaseData( | ||||
|                 new[] { typeof(HelloWorldDefaultCommand) }, | ||||
|                 new[] { "--help" }, | ||||
|                 null | ||||
|             ); | ||||
|  | ||||
|             yield return new TestCaseData( | ||||
|                 new[] { typeof(ConcatCommand) }, | ||||
|                 new string[0], | ||||
|                 null | ||||
|             ); | ||||
|  | ||||
|             yield return new TestCaseData( | ||||
|                 new[] { typeof(ConcatCommand) }, | ||||
|                 new[] { "-h" }, | ||||
|                 null | ||||
|             ); | ||||
|  | ||||
|             yield return new TestCaseData( | ||||
|                 new[] { typeof(ConcatCommand) }, | ||||
|                 new[] { "--help" }, | ||||
|                 null | ||||
|             ); | ||||
|  | ||||
|             yield return new TestCaseData( | ||||
|                 new[] { typeof(ConcatCommand) }, | ||||
|                 new[] { "concat", "-h" }, | ||||
|                 null | ||||
|             ); | ||||
|  | ||||
|             yield return new TestCaseData( | ||||
|                 new[] { typeof(ExceptionCommand) }, | ||||
|                 new[] { "exc", "-h" }, | ||||
|                 null | ||||
|             ); | ||||
|  | ||||
|             yield return new TestCaseData( | ||||
|                 new[] { typeof(CommandExceptionCommand) }, | ||||
|                 new[] { "exc", "-h" }, | ||||
|                 null | ||||
|             ); | ||||
|  | ||||
|             yield return new TestCaseData( | ||||
|                 new[] { typeof(ConcatCommand) }, | ||||
|                 new[] { "[preview]" }, | ||||
|                 null | ||||
|             ); | ||||
|  | ||||
|             yield return new TestCaseData( | ||||
|                 new[] { typeof(ExceptionCommand) }, | ||||
|                 new[] { "exc", "[preview]" }, | ||||
|                 null | ||||
|             ); | ||||
|  | ||||
|             yield return new TestCaseData( | ||||
|                 new[] { typeof(ConcatCommand) }, | ||||
|                 new[] { "concat", "[preview]", "-o", "value" }, | ||||
|                 null | ||||
|             ); | ||||
|         } | ||||
|  | ||||
|         private static IEnumerable<TestCaseData> GetTestCases_RunAsync_Negative() | ||||
|         { | ||||
|             yield return new TestCaseData( | ||||
|                 new Type[0], | ||||
|                 new string[0], | ||||
|                 null, null | ||||
|             ); | ||||
|  | ||||
|             yield return new TestCaseData( | ||||
|                 new[] { typeof(ConcatCommand) }, | ||||
|                 new[] { "non-existing" }, | ||||
|                 null, null | ||||
|             ); | ||||
|  | ||||
|             yield return new TestCaseData( | ||||
|                 new[] { typeof(ExceptionCommand) }, | ||||
|                 new[] { "exc" }, | ||||
|                 null, null | ||||
|             ); | ||||
|  | ||||
|             yield return new TestCaseData( | ||||
|                 new[] { typeof(CommandExceptionCommand) }, | ||||
|                 new[] { "exc" }, | ||||
|                 null, null | ||||
|             ); | ||||
|  | ||||
|             yield return new TestCaseData( | ||||
|                 new[] { typeof(CommandExceptionCommand) }, | ||||
|                 new[] { "exc" }, | ||||
|                 null, null | ||||
|             ); | ||||
|  | ||||
|             yield return new TestCaseData( | ||||
|                 new[] { typeof(CommandExceptionCommand) }, | ||||
|                 new[] { "exc", "-m", "foo bar" }, | ||||
|                 "foo bar", null | ||||
|             ); | ||||
|  | ||||
|             yield return new TestCaseData( | ||||
|                 new[] { typeof(CommandExceptionCommand) }, | ||||
|                 new[] { "exc", "-m", "foo bar", "-c", "666" }, | ||||
|                 "foo bar", 666 | ||||
|             ); | ||||
|         } | ||||
|  | ||||
|         [Test] | ||||
|         [TestCaseSource(nameof(GetTestCases_RunAsync))] | ||||
|         public async Task RunAsync_Test(IReadOnlyList<Type> commandTypes, IReadOnlyList<string> commandLineArguments, | ||||
|             string expectedStdOut = null) | ||||
|         { | ||||
|             // Arrange | ||||
|             using (var stdoutStream = new StringWriter()) | ||||
|             { | ||||
|                 var console = new VirtualConsole(stdoutStream); | ||||
|                 var environmentVariablesProvider = new EnvironmentVariablesProviderStub(); | ||||
|  | ||||
|                 var application = new CliApplicationBuilder() | ||||
|                     .AddCommands(commandTypes) | ||||
|                     .UseVersionText(TestVersionText) | ||||
|                     .UseConsole(console) | ||||
|                     .UseEnvironmentVariablesProvider(environmentVariablesProvider) | ||||
|                     .Build(); | ||||
|  | ||||
|                 // Act | ||||
|                 var exitCode = await application.RunAsync(commandLineArguments); | ||||
|                 var stdOut = stdoutStream.ToString().Trim(); | ||||
|  | ||||
|                 // Assert | ||||
|                 exitCode.Should().Be(0); | ||||
|  | ||||
|                 if (expectedStdOut != null) | ||||
|                     stdOut.Should().Be(expectedStdOut); | ||||
|                 else | ||||
|                     stdOut.Should().NotBeNullOrWhiteSpace(); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         [Test] | ||||
|         [TestCaseSource(nameof(GetTestCases_RunAsync_Negative))] | ||||
|         public async Task RunAsync_Negative_Test(IReadOnlyList<Type> commandTypes, IReadOnlyList<string> commandLineArguments, | ||||
|             string expectedStdErr = null, int? expectedExitCode = null) | ||||
|         { | ||||
|             // Arrange | ||||
|             using (var stderrStream = new StringWriter()) | ||||
|             { | ||||
|                 var console = new VirtualConsole(TextWriter.Null, stderrStream); | ||||
|                 var environmentVariablesProvider = new EnvironmentVariablesProviderStub(); | ||||
|  | ||||
|                 var application = new CliApplicationBuilder() | ||||
|                     .AddCommands(commandTypes) | ||||
|                     .UseVersionText(TestVersionText) | ||||
|                     .UseEnvironmentVariablesProvider(environmentVariablesProvider) | ||||
|                     .UseConsole(console) | ||||
|                     .Build(); | ||||
|  | ||||
|                 // Act | ||||
|                 var exitCode = await application.RunAsync(commandLineArguments); | ||||
|                 var stderr = stderrStream.ToString().Trim(); | ||||
|  | ||||
|                 // Assert | ||||
|                 if (expectedExitCode != null) | ||||
|                     exitCode.Should().Be(expectedExitCode); | ||||
|                 else | ||||
|                     exitCode.Should().NotBe(0); | ||||
|  | ||||
|                 if (expectedStdErr != null) | ||||
|                     stderr.Should().Be(expectedStdErr); | ||||
|                 else | ||||
|                     stderr.Should().NotBeNullOrWhiteSpace(); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         [Test] | ||||
|         public async Task RunAsync_Cancellation_Test() | ||||
|         { | ||||
|             // Arrange | ||||
|             using (var stdoutStream = new StringWriter()) | ||||
|             { | ||||
|                 var console = new VirtualConsole(stdoutStream); | ||||
|                  | ||||
|                 var application = new CliApplicationBuilder() | ||||
|                     .AddCommand(typeof(CancellableCommand)) | ||||
|                     .UseConsole(console) | ||||
|                     .Build(); | ||||
|                 var args = new[] { "cancel" }; | ||||
|  | ||||
|                 // Act | ||||
|                 var runTask = application.RunAsync(args); | ||||
|                 console.Cancel(); | ||||
|                 var exitCode = await runTask.ConfigureAwait(false); | ||||
|                 var stdOut = stdoutStream.ToString().Trim(); | ||||
|  | ||||
|                 // Assert | ||||
|                 exitCode.Should().Be(-2146233029); | ||||
|                 stdOut.Should().Be("Printed"); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -1,26 +1,30 @@ | ||||
| <Project Sdk="Microsoft.NET.Sdk"> | ||||
|  | ||||
|   <PropertyGroup> | ||||
|     <TargetFramework>net46</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="FluentAssertions" Version="5.8.0" /> | ||||
|     <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.2.0" /> | ||||
|     <PackageReference Include="NUnit" Version="3.12.0" /> | ||||
|     <PackageReference Include="NUnit3TestAdapter" Version="3.14.0" /> | ||||
|     <PackageReference Include="coverlet.msbuild" Version="2.6.3"> | ||||
|       <PrivateAssets>all</PrivateAssets> | ||||
|       <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> | ||||
|     </PackageReference> | ||||
|     <Content Include="xunit.runner.json" CopyToOutputDirectory="PreserveNewest" /> | ||||
|   </ItemGroup> | ||||
|  | ||||
|   <ItemGroup> | ||||
|     <PackageReference Include="Basic.Reference.Assemblies.Net80" Version="1.8.0" /> | ||||
|     <PackageReference Include="CliWrap" Version="3.7.1" /> | ||||
|     <PackageReference Include="coverlet.collector" Version="6.0.4" PrivateAssets="all" /> | ||||
|     <PackageReference Include="CSharpier.MsBuild" Version="0.30.6" PrivateAssets="all" /> | ||||
|     <PackageReference Include="FluentAssertions" Version="8.0.1" /> | ||||
|     <PackageReference Include="GitHubActionsTestLogger" Version="2.4.1" PrivateAssets="all" /> | ||||
|     <PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.11.0" /> | ||||
|     <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.1" /> | ||||
|     <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" /> | ||||
|     <PackageReference Include="PolyShim" Version="1.14.0" PrivateAssets="all" /> | ||||
|     <PackageReference Include="xunit" Version="2.9.3" /> | ||||
|     <PackageReference Include="xunit.runner.visualstudio" Version="3.0.1" PrivateAssets="all" /> | ||||
|   </ItemGroup> | ||||
|  | ||||
|   <ItemGroup> | ||||
|     <ProjectReference Include="..\CliFx.Tests.Dummy\CliFx.Tests.Dummy.csproj" /> | ||||
|     <ProjectReference Include="..\CliFx\CliFx.csproj" /> | ||||
|   </ItemGroup> | ||||
|  | ||||
|   | ||||
							
								
								
									
										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"); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										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\"" | ||||
|             ); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										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"); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										994
									
								
								CliFx.Tests/HelpTextSpecs.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										994
									
								
								CliFx.Tests/HelpTextSpecs.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,994 @@ | ||||
| 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 HelpTextSpecs(ITestOutputHelper testOutput) : SpecsBase(testOutput) | ||||
| { | ||||
|     [Fact] | ||||
|     public async Task I_can_request_the_help_text_by_running_the_application_without_arguments_if_the_default_command_is_not_defined() | ||||
|     { | ||||
|         // Arrange | ||||
|         var application = new CliApplicationBuilder() | ||||
|             .UseConsole(FakeConsole) | ||||
|             .SetDescription("This will be in help text") | ||||
|             .Build(); | ||||
|  | ||||
|         // Act | ||||
|         var exitCode = await application.RunAsync( | ||||
|             [], | ||||
|             new Dictionary<string, string>() | ||||
|         ); | ||||
|  | ||||
|         // Assert | ||||
|         exitCode.Should().Be(0); | ||||
|  | ||||
|         var stdOut = FakeConsole.ReadOutputString(); | ||||
|         stdOut.Should().Contain("This will be in help text"); | ||||
|     } | ||||
|  | ||||
|     [Fact] | ||||
|     public async Task I_can_request_the_help_text_by_running_the_application_with_the_help_option() | ||||
|     { | ||||
|         // Arrange | ||||
|         var commandType = DynamicCommandBuilder.Compile( | ||||
|             // lang=csharp | ||||
|             """ | ||||
|             [Command] | ||||
|             public class DefaultCommand : ICommand | ||||
|             { | ||||
|                 public ValueTask ExecuteAsync(IConsole console) => default; | ||||
|             } | ||||
|             """ | ||||
|         ); | ||||
|  | ||||
|         var application = new CliApplicationBuilder() | ||||
|             .AddCommand(commandType) | ||||
|             .UseConsole(FakeConsole) | ||||
|             .SetDescription("This will be in help text") | ||||
|             .Build(); | ||||
|  | ||||
|         // Act | ||||
|         var exitCode = await application.RunAsync(["--help"], new Dictionary<string, string>()); | ||||
|  | ||||
|         // Assert | ||||
|         exitCode.Should().Be(0); | ||||
|  | ||||
|         var stdOut = FakeConsole.ReadOutputString(); | ||||
|         stdOut.Should().Contain("This will be in help text"); | ||||
|     } | ||||
|  | ||||
|     [Fact] | ||||
|     public async Task I_can_request_the_help_text_by_running_the_application_with_the_help_option_even_if_the_default_command_is_not_defined() | ||||
|     { | ||||
|         // Arrange | ||||
|         var commandTypes = DynamicCommandBuilder.CompileMany( | ||||
|             // lang=csharp | ||||
|             """ | ||||
|             [Command("cmd")] | ||||
|             public class NamedCommand : ICommand | ||||
|             { | ||||
|                 public ValueTask ExecuteAsync(IConsole console) => default; | ||||
|             } | ||||
|  | ||||
|             [Command("cmd child")] | ||||
|             public class NamedChildCommand : ICommand | ||||
|             { | ||||
|                 public ValueTask ExecuteAsync(IConsole console) => default; | ||||
|             } | ||||
|             """ | ||||
|         ); | ||||
|  | ||||
|         var application = new CliApplicationBuilder() | ||||
|             .AddCommands(commandTypes) | ||||
|             .UseConsole(FakeConsole) | ||||
|             .SetDescription("This will be in help text") | ||||
|             .Build(); | ||||
|  | ||||
|         // Act | ||||
|         var exitCode = await application.RunAsync(["--help"], new Dictionary<string, string>()); | ||||
|  | ||||
|         // Assert | ||||
|         exitCode.Should().Be(0); | ||||
|  | ||||
|         var stdOut = FakeConsole.ReadOutputString(); | ||||
|         stdOut.Should().Contain("This will be in help text"); | ||||
|     } | ||||
|  | ||||
|     [Fact] | ||||
|     public async Task I_can_request_the_help_text_for_a_specific_command_by_running_the_application_and_specifying_its_name_with_the_help_option() | ||||
|     { | ||||
|         // Arrange | ||||
|         var commandTypes = DynamicCommandBuilder.CompileMany( | ||||
|             // lang=csharp | ||||
|             """ | ||||
|             [Command] | ||||
|             public class DefaultCommand : ICommand | ||||
|             { | ||||
|                 public ValueTask ExecuteAsync(IConsole console) => default; | ||||
|             } | ||||
|  | ||||
|             [Command("cmd", Description = "Description of a named command.")] | ||||
|             public class NamedCommand : ICommand | ||||
|             { | ||||
|                 public ValueTask ExecuteAsync(IConsole console) => default; | ||||
|             } | ||||
|  | ||||
|             [Command("cmd child")] | ||||
|             public class NamedChildCommand : ICommand | ||||
|             { | ||||
|                 public ValueTask ExecuteAsync(IConsole console) => default; | ||||
|             } | ||||
|             """ | ||||
|         ); | ||||
|  | ||||
|         var application = new CliApplicationBuilder() | ||||
|             .AddCommands(commandTypes) | ||||
|             .UseConsole(FakeConsole) | ||||
|             .Build(); | ||||
|  | ||||
|         // Act | ||||
|         var exitCode = await application.RunAsync( | ||||
|             ["cmd", "--help"], | ||||
|             new Dictionary<string, string>() | ||||
|         ); | ||||
|  | ||||
|         // Assert | ||||
|         exitCode.Should().Be(0); | ||||
|  | ||||
|         var stdOut = FakeConsole.ReadOutputString(); | ||||
|         stdOut.Should().Contain("Description of a named command."); | ||||
|     } | ||||
|  | ||||
|     [Fact] | ||||
|     public async Task I_can_request_the_help_text_for_a_specific_nested_command_by_running_the_application_and_specifying_its_name_with_the_help_option() | ||||
|     { | ||||
|         // Arrange | ||||
|         var commandTypes = DynamicCommandBuilder.CompileMany( | ||||
|             // lang=csharp | ||||
|             """ | ||||
|             [Command] | ||||
|             public class DefaultCommand : ICommand | ||||
|             { | ||||
|                 public ValueTask ExecuteAsync(IConsole console) => default; | ||||
|             } | ||||
|  | ||||
|             [Command("cmd")] | ||||
|             public class NamedCommand : ICommand | ||||
|             { | ||||
|                 public ValueTask ExecuteAsync(IConsole console) => default; | ||||
|             } | ||||
|  | ||||
|             [Command("cmd child", Description = "Description of a named child command.")] | ||||
|             public class NamedChildCommand : ICommand | ||||
|             { | ||||
|                 public ValueTask ExecuteAsync(IConsole console) => default; | ||||
|             } | ||||
|             """ | ||||
|         ); | ||||
|  | ||||
|         var application = new CliApplicationBuilder() | ||||
|             .AddCommands(commandTypes) | ||||
|             .UseConsole(FakeConsole) | ||||
|             .Build(); | ||||
|  | ||||
|         // Act | ||||
|         var exitCode = await application.RunAsync( | ||||
|             ["cmd", "sub", "--help"], | ||||
|             new Dictionary<string, string>() | ||||
|         ); | ||||
|  | ||||
|         // Assert | ||||
|         exitCode.Should().Be(0); | ||||
|  | ||||
|         var stdOut = FakeConsole.ReadOutputString(); | ||||
|         stdOut.Should().Contain("Description of a named child command."); | ||||
|     } | ||||
|  | ||||
|     [Fact] | ||||
|     public async Task I_can_request_the_help_text_by_running_the_application_with_invalid_arguments() | ||||
|     { | ||||
|         // Arrange | ||||
|         var application = new CliApplicationBuilder() | ||||
|             .AddCommand<NoOpCommand>() | ||||
|             .UseConsole(FakeConsole) | ||||
|             .SetDescription("This will be in help text") | ||||
|             .Build(); | ||||
|  | ||||
|         // Act | ||||
|         var exitCode = await application.RunAsync( | ||||
|             ["invalid-command", "--invalid-option"], | ||||
|             new Dictionary<string, string>() | ||||
|         ); | ||||
|  | ||||
|         // Assert | ||||
|         exitCode.Should().NotBe(0); | ||||
|  | ||||
|         var stdOut = FakeConsole.ReadOutputString(); | ||||
|         stdOut.Should().Contain("This will be in help text"); | ||||
|  | ||||
|         var stdErr = FakeConsole.ReadErrorString(); | ||||
|         stdErr.Should().NotBeNullOrWhiteSpace(); | ||||
|     } | ||||
|  | ||||
|     [Fact] | ||||
|     public async Task I_can_request_the_help_text_to_see_the_application_title_description_and_version() | ||||
|     { | ||||
|         // Arrange | ||||
|         var application = new CliApplicationBuilder() | ||||
|             .UseConsole(FakeConsole) | ||||
|             .SetTitle("App title") | ||||
|             .SetDescription("App description") | ||||
|             .SetVersion("App version") | ||||
|             .Build(); | ||||
|  | ||||
|         // Act | ||||
|         var exitCode = await application.RunAsync(["--help"], new Dictionary<string, string>()); | ||||
|  | ||||
|         // Assert | ||||
|         exitCode.Should().Be(0); | ||||
|  | ||||
|         var stdOut = FakeConsole.ReadOutputString(); | ||||
|         stdOut.Should().ContainAll("App title", "App description", "App version"); | ||||
|     } | ||||
|  | ||||
|     [Fact] | ||||
|     public async Task I_can_request_the_help_text_to_see_the_command_description() | ||||
|     { | ||||
|         // Arrange | ||||
|         var commandType = DynamicCommandBuilder.Compile( | ||||
|             // lang=csharp | ||||
|             """ | ||||
|             [Command(Description = "Description of the default command.")] | ||||
|             public class DefaultCommand : ICommand | ||||
|             { | ||||
|                 public ValueTask ExecuteAsync(IConsole console) => default; | ||||
|             } | ||||
|             """ | ||||
|         ); | ||||
|  | ||||
|         var application = new CliApplicationBuilder() | ||||
|             .AddCommand(commandType) | ||||
|             .UseConsole(FakeConsole) | ||||
|             .Build(); | ||||
|  | ||||
|         // Act | ||||
|         var exitCode = await application.RunAsync(["--help"], new Dictionary<string, string>()); | ||||
|  | ||||
|         // Assert | ||||
|         exitCode.Should().Be(0); | ||||
|  | ||||
|         var stdOut = FakeConsole.ReadOutputString(); | ||||
|         stdOut.Should().ContainAllInOrder("DESCRIPTION", "Description of the default command."); | ||||
|     } | ||||
|  | ||||
|     [Fact] | ||||
|     public async Task I_can_request_the_help_text_to_see_the_usage_format_for_a_named_command() | ||||
|     { | ||||
|         // Arrange | ||||
|         var commandTypes = DynamicCommandBuilder.CompileMany( | ||||
|             // lang=csharp | ||||
|             """ | ||||
|             [Command] | ||||
|             public class DefaultCommand : ICommand | ||||
|             { | ||||
|                 public ValueTask ExecuteAsync(IConsole console) => default; | ||||
|             } | ||||
|  | ||||
|             [Command("cmd")] | ||||
|             public class NamedCommand : ICommand | ||||
|             { | ||||
|                 public ValueTask ExecuteAsync(IConsole console) => default; | ||||
|             } | ||||
|             """ | ||||
|         ); | ||||
|  | ||||
|         var application = new CliApplicationBuilder() | ||||
|             .AddCommands(commandTypes) | ||||
|             .UseConsole(FakeConsole) | ||||
|             .Build(); | ||||
|  | ||||
|         // Act | ||||
|         var exitCode = await application.RunAsync(["--help"], new Dictionary<string, string>()); | ||||
|  | ||||
|         // Assert | ||||
|         exitCode.Should().Be(0); | ||||
|  | ||||
|         var stdOut = FakeConsole.ReadOutputString(); | ||||
|         stdOut.Should().ContainAllInOrder("USAGE", "[command]", "[...]"); | ||||
|     } | ||||
|  | ||||
|     [Fact] | ||||
|     public async Task I_can_request_the_help_text_to_see_the_usage_format_for_all_parameters() | ||||
|     { | ||||
|         // 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; } | ||||
|  | ||||
|                 public ValueTask ExecuteAsync(IConsole console) => default; | ||||
|             } | ||||
|             """ | ||||
|         ); | ||||
|  | ||||
|         var application = new CliApplicationBuilder() | ||||
|             .AddCommand(commandType) | ||||
|             .UseConsole(FakeConsole) | ||||
|             .Build(); | ||||
|  | ||||
|         // Act | ||||
|         var exitCode = await application.RunAsync(["--help"], new Dictionary<string, string>()); | ||||
|  | ||||
|         // Assert | ||||
|         exitCode.Should().Be(0); | ||||
|  | ||||
|         var stdOut = FakeConsole.ReadOutputString(); | ||||
|         stdOut.Should().ContainAllInOrder("USAGE", "<foo>", "<bar>", "<baz...>"); | ||||
|     } | ||||
|  | ||||
|     // https://github.com/Tyrrrz/CliFx/issues/117 | ||||
|     [Fact] | ||||
|     public async Task I_can_request_the_help_text_to_see_the_usage_format_for_all_parameters_in_the_correct_order() | ||||
|     { | ||||
|         // Arrange | ||||
|         var commandType = DynamicCommandBuilder.Compile( | ||||
|             // lang=csharp | ||||
|             """ | ||||
|             // Base members appear last in reflection order | ||||
|             public abstract class CommandBase : ICommand | ||||
|             { | ||||
|                 [CommandParameter(0)] | ||||
|                 public required string Foo { get; init; } | ||||
|  | ||||
|                 public abstract ValueTask ExecuteAsync(IConsole console); | ||||
|             } | ||||
|  | ||||
|             [Command] | ||||
|             public class Command : CommandBase | ||||
|             { | ||||
|                 [CommandParameter(2)] | ||||
|                 public required IReadOnlyList<string> Baz { get; init; } | ||||
|  | ||||
|                 [CommandParameter(1)] | ||||
|                 public required string Bar { get; init; } | ||||
|  | ||||
|                 public override ValueTask ExecuteAsync(IConsole console) => default; | ||||
|             } | ||||
|             """ | ||||
|         ); | ||||
|  | ||||
|         var application = new CliApplicationBuilder() | ||||
|             .AddCommand(commandType) | ||||
|             .UseConsole(FakeConsole) | ||||
|             .Build(); | ||||
|  | ||||
|         // Act | ||||
|         var exitCode = await application.RunAsync(["--help"], new Dictionary<string, string>()); | ||||
|  | ||||
|         // Assert | ||||
|         exitCode.Should().Be(0); | ||||
|  | ||||
|         var stdOut = FakeConsole.ReadOutputString(); | ||||
|         stdOut.Should().ContainAllInOrder("USAGE", "<foo>", "<bar>", "<baz...>"); | ||||
|     } | ||||
|  | ||||
|     [Fact] | ||||
|     public async Task I_can_request_the_help_text_to_see_the_usage_format_for_all_required_options() | ||||
|     { | ||||
|         // Arrange | ||||
|         var commandType = DynamicCommandBuilder.Compile( | ||||
|             // lang=csharp | ||||
|             """ | ||||
|             [Command] | ||||
|             public class Command : ICommand | ||||
|             { | ||||
|                 [CommandOption("foo")] | ||||
|                 public required string Foo { get; init; } | ||||
|  | ||||
|                 [CommandOption("bar")] | ||||
|                 public string? Bar { get; init; } | ||||
|  | ||||
|                 [CommandOption("baz")] | ||||
|                 public required IReadOnlyList<string> Baz { get; init; } | ||||
|  | ||||
|                 public ValueTask ExecuteAsync(IConsole console) => default; | ||||
|             } | ||||
|             """ | ||||
|         ); | ||||
|  | ||||
|         var application = new CliApplicationBuilder() | ||||
|             .AddCommand(commandType) | ||||
|             .UseConsole(FakeConsole) | ||||
|             .Build(); | ||||
|  | ||||
|         // Act | ||||
|         var exitCode = await application.RunAsync(["--help"], new Dictionary<string, string>()); | ||||
|  | ||||
|         // Assert | ||||
|         exitCode.Should().Be(0); | ||||
|  | ||||
|         var stdOut = FakeConsole.ReadOutputString(); | ||||
|         stdOut | ||||
|             .Should() | ||||
|             .ContainAllInOrder("USAGE", "--foo <value>", "--baz <values...>", "[options]"); | ||||
|     } | ||||
|  | ||||
|     [Fact] | ||||
|     public async Task I_can_request_the_help_text_to_see_the_list_of_all_parameters_and_options() | ||||
|     { | ||||
|         // Arrange | ||||
|         var commandType = DynamicCommandBuilder.Compile( | ||||
|             // lang=csharp | ||||
|             """ | ||||
|             [Command] | ||||
|             public class Command : ICommand | ||||
|             { | ||||
|                 [CommandParameter(0, Name = "foo", Description = "Description of foo.")] | ||||
|                 public string? Foo { get; init; } | ||||
|  | ||||
|                 [CommandOption("bar", Description = "Description of bar.")] | ||||
|                 public 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(["--help"], new Dictionary<string, string>()); | ||||
|  | ||||
|         // Assert | ||||
|         exitCode.Should().Be(0); | ||||
|  | ||||
|         var stdOut = FakeConsole.ReadOutputString(); | ||||
|         stdOut | ||||
|             .Should() | ||||
|             .ContainAllInOrder( | ||||
|                 "PARAMETERS", | ||||
|                 "foo", | ||||
|                 "Description of foo.", | ||||
|                 "OPTIONS", | ||||
|                 "--bar", | ||||
|                 "Description of bar." | ||||
|             ); | ||||
|     } | ||||
|  | ||||
|     [Fact] | ||||
|     public async Task I_can_request_the_help_text_to_see_the_help_and_version_options() | ||||
|     { | ||||
|         // Arrange | ||||
|         var commandType = DynamicCommandBuilder.Compile( | ||||
|             // lang=csharp | ||||
|             """ | ||||
|             [Command] | ||||
|             public class Command : ICommand | ||||
|             { | ||||
|                 public ValueTask ExecuteAsync(IConsole console) => default; | ||||
|             } | ||||
|             """ | ||||
|         ); | ||||
|  | ||||
|         var application = new CliApplicationBuilder() | ||||
|             .AddCommand(commandType) | ||||
|             .UseConsole(FakeConsole) | ||||
|             .Build(); | ||||
|  | ||||
|         // Act | ||||
|         var exitCode = await application.RunAsync(["--help"], new Dictionary<string, string>()); | ||||
|  | ||||
|         // Assert | ||||
|         exitCode.Should().Be(0); | ||||
|  | ||||
|         var stdOut = FakeConsole.ReadOutputString(); | ||||
|         stdOut | ||||
|             .Should() | ||||
|             .ContainAllInOrder( | ||||
|                 "OPTIONS", | ||||
|                 "-h", | ||||
|                 "--help", | ||||
|                 "Shows help text", | ||||
|                 "--version", | ||||
|                 "Shows version information" | ||||
|             ); | ||||
|     } | ||||
|  | ||||
|     [Fact] | ||||
|     public async Task I_can_request_the_help_text_on_a_named_command_to_see_the_help_option() | ||||
|     { | ||||
|         // 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) | ||||
|             .Build(); | ||||
|  | ||||
|         // Act | ||||
|         var exitCode = await application.RunAsync( | ||||
|             ["cmd", "--help"], | ||||
|             new Dictionary<string, string>() | ||||
|         ); | ||||
|  | ||||
|         // Assert | ||||
|         exitCode.Should().Be(0); | ||||
|  | ||||
|         var stdOut = FakeConsole.ReadOutputString(); | ||||
|  | ||||
|         stdOut.Should().ContainAllInOrder("OPTIONS", "-h", "--help", "Shows help text"); | ||||
|  | ||||
|         stdOut.Should().NotContainAny("--version", "Shows version information"); | ||||
|     } | ||||
|  | ||||
|     [Fact] | ||||
|     public async Task I_can_request_the_help_text_to_see_the_list_of_valid_values_for_all_parameters_and_options_bound_to_enum_properties() | ||||
|     { | ||||
|         // Arrange | ||||
|         var commandType = DynamicCommandBuilder.Compile( | ||||
|             // lang=csharp | ||||
|             """ | ||||
|             public enum CustomEnum { One, Two, Three } | ||||
|  | ||||
|             [Command] | ||||
|             public class Command : ICommand | ||||
|             { | ||||
|                 [CommandParameter(0)] | ||||
|                 public CustomEnum Foo { get; init; } | ||||
|  | ||||
|                 [CommandOption("bar")] | ||||
|                 public CustomEnum Bar { get; init; } | ||||
|  | ||||
|                 public ValueTask ExecuteAsync(IConsole console) => default; | ||||
|             } | ||||
|             """ | ||||
|         ); | ||||
|  | ||||
|         var application = new CliApplicationBuilder() | ||||
|             .AddCommand(commandType) | ||||
|             .UseConsole(FakeConsole) | ||||
|             .Build(); | ||||
|  | ||||
|         // Act | ||||
|         var exitCode = await application.RunAsync(["--help"], new Dictionary<string, string>()); | ||||
|  | ||||
|         // Assert | ||||
|         exitCode.Should().Be(0); | ||||
|  | ||||
|         var stdOut = FakeConsole.ReadOutputString(); | ||||
|         stdOut | ||||
|             .Should() | ||||
|             .ContainAllInOrder( | ||||
|                 "PARAMETERS", | ||||
|                 "foo", | ||||
|                 "Choices:", | ||||
|                 "One", | ||||
|                 "Two", | ||||
|                 "Three", | ||||
|                 "OPTIONS", | ||||
|                 "--bar", | ||||
|                 "Choices:", | ||||
|                 "One", | ||||
|                 "Two", | ||||
|                 "Three" | ||||
|             ); | ||||
|     } | ||||
|  | ||||
|     [Fact] | ||||
|     public async Task I_can_request_the_help_text_to_see_the_list_of_valid_values_for_all_parameters_and_options_bound_to_non_scalar_enum_properties() | ||||
|     { | ||||
|         // Arrange | ||||
|         var commandType = DynamicCommandBuilder.Compile( | ||||
|             // lang=csharp | ||||
|             """ | ||||
|             public enum CustomEnum { One, Two, Three } | ||||
|  | ||||
|             [Command] | ||||
|             public class Command : ICommand | ||||
|             { | ||||
|                 [CommandParameter(0)] | ||||
|                 public required IReadOnlyList<CustomEnum> Foo { get; init; } | ||||
|  | ||||
|                 [CommandOption("bar")] | ||||
|                 public required IReadOnlyList<CustomEnum> Bar { get; init; } | ||||
|  | ||||
|                 public ValueTask ExecuteAsync(IConsole console) => default; | ||||
|             } | ||||
|             """ | ||||
|         ); | ||||
|  | ||||
|         var application = new CliApplicationBuilder() | ||||
|             .AddCommand(commandType) | ||||
|             .UseConsole(FakeConsole) | ||||
|             .Build(); | ||||
|  | ||||
|         // Act | ||||
|         var exitCode = await application.RunAsync(["--help"], new Dictionary<string, string>()); | ||||
|  | ||||
|         // Assert | ||||
|         exitCode.Should().Be(0); | ||||
|  | ||||
|         var stdOut = FakeConsole.ReadOutputString(); | ||||
|         stdOut | ||||
|             .Should() | ||||
|             .ContainAllInOrder( | ||||
|                 "PARAMETERS", | ||||
|                 "foo", | ||||
|                 "Choices:", | ||||
|                 "One", | ||||
|                 "Two", | ||||
|                 "Three", | ||||
|                 "OPTIONS", | ||||
|                 "--bar", | ||||
|                 "Choices:", | ||||
|                 "One", | ||||
|                 "Two", | ||||
|                 "Three" | ||||
|             ); | ||||
|     } | ||||
|  | ||||
|     [Fact] | ||||
|     public async Task I_can_request_the_help_text_to_see_the_list_of_valid_values_for_all_parameters_and_options_bound_to_nullable_enum_properties() | ||||
|     { | ||||
|         // Arrange | ||||
|         var commandType = DynamicCommandBuilder.Compile( | ||||
|             // lang=csharp | ||||
|             """ | ||||
|             public enum CustomEnum { One, Two, Three } | ||||
|  | ||||
|             [Command] | ||||
|             public class Command : ICommand | ||||
|             { | ||||
|                 [CommandParameter(0)] | ||||
|                 public CustomEnum? Foo { get; init; } | ||||
|  | ||||
|                 [CommandOption("bar")] | ||||
|                 public IReadOnlyList<CustomEnum?>? Bar { get; init; } | ||||
|  | ||||
|                 public ValueTask ExecuteAsync(IConsole console) => default; | ||||
|             } | ||||
|             """ | ||||
|         ); | ||||
|  | ||||
|         var application = new CliApplicationBuilder() | ||||
|             .AddCommand(commandType) | ||||
|             .UseConsole(FakeConsole) | ||||
|             .Build(); | ||||
|  | ||||
|         // Act | ||||
|         var exitCode = await application.RunAsync(["--help"], new Dictionary<string, string>()); | ||||
|  | ||||
|         // Assert | ||||
|         exitCode.Should().Be(0); | ||||
|  | ||||
|         var stdOut = FakeConsole.ReadOutputString(); | ||||
|         stdOut | ||||
|             .Should() | ||||
|             .ContainAllInOrder( | ||||
|                 "PARAMETERS", | ||||
|                 "foo", | ||||
|                 "Choices:", | ||||
|                 "One", | ||||
|                 "Two", | ||||
|                 "Three", | ||||
|                 "OPTIONS", | ||||
|                 "--bar", | ||||
|                 "Choices:", | ||||
|                 "One", | ||||
|                 "Two", | ||||
|                 "Three" | ||||
|             ); | ||||
|     } | ||||
|  | ||||
|     [Fact] | ||||
|     public async Task I_can_request_the_help_text_to_see_the_environment_variables_of_options_that_use_them_as_fallback() | ||||
|     { | ||||
|         // Arrange | ||||
|         var commandType = DynamicCommandBuilder.Compile( | ||||
|             // lang=csharp | ||||
|             """ | ||||
|             public enum CustomEnum { One, Two, Three } | ||||
|  | ||||
|             [Command] | ||||
|             public class Command : ICommand | ||||
|             { | ||||
|                 [CommandOption("foo", EnvironmentVariable = "ENV_FOO")] | ||||
|                 public CustomEnum Foo { get; init; } | ||||
|  | ||||
|                 [CommandOption("bar", EnvironmentVariable = "ENV_BAR")] | ||||
|                 public CustomEnum Bar { get; init; } | ||||
|  | ||||
|                 public ValueTask ExecuteAsync(IConsole console) => default; | ||||
|             } | ||||
|             """ | ||||
|         ); | ||||
|  | ||||
|         var application = new CliApplicationBuilder() | ||||
|             .AddCommand(commandType) | ||||
|             .UseConsole(FakeConsole) | ||||
|             .Build(); | ||||
|  | ||||
|         // Act | ||||
|         var exitCode = await application.RunAsync(["--help"], new Dictionary<string, string>()); | ||||
|  | ||||
|         // Assert | ||||
|         exitCode.Should().Be(0); | ||||
|  | ||||
|         var stdOut = FakeConsole.ReadOutputString(); | ||||
|         stdOut | ||||
|             .Should() | ||||
|             .ContainAllInOrder( | ||||
|                 "OPTIONS", | ||||
|                 "--foo", | ||||
|                 "Environment variable:", | ||||
|                 "ENV_FOO", | ||||
|                 "--bar", | ||||
|                 "Environment variable:", | ||||
|                 "ENV_BAR" | ||||
|             ); | ||||
|     } | ||||
|  | ||||
|     [Fact] | ||||
|     public async Task I_can_request_the_help_text_to_see_the_default_values_of_non_required_options() | ||||
|     { | ||||
|         // Arrange | ||||
|         var commandType = DynamicCommandBuilder.Compile( | ||||
|             // lang=csharp | ||||
|             """ | ||||
|             public enum CustomEnum { One, Two, Three } | ||||
|  | ||||
|             [Command] | ||||
|             public class Command : ICommand | ||||
|             { | ||||
|                 [CommandOption("foo")] | ||||
|                 public object? Foo { get; init; } = 42; | ||||
|  | ||||
|                 [CommandOption("bar")] | ||||
|                 public string? Bar { get; init; } = "hello"; | ||||
|  | ||||
|                 [CommandOption("baz")] | ||||
|                 public IReadOnlyList<string>? Baz { get; init; } = new[] {"one", "two", "three"}; | ||||
|  | ||||
|                 [CommandOption("qwe")] | ||||
|                 public bool Qwe { get; init; } = true; | ||||
|  | ||||
|                 [CommandOption("qop")] | ||||
|                 public int? Qop { get; init; } = 1337; | ||||
|  | ||||
|                 [CommandOption("zor")] | ||||
|                 public TimeSpan Zor { get; init; } = TimeSpan.FromMinutes(123); | ||||
|  | ||||
|                 [CommandOption("lol")] | ||||
|                 public CustomEnum Lol { get; init; } = CustomEnum.Two; | ||||
|  | ||||
|                 [CommandOption("hmm")] | ||||
|                 public required string Hmm { get; init; } = "not printed"; | ||||
|  | ||||
|                 public ValueTask ExecuteAsync(IConsole console) => default; | ||||
|             } | ||||
|             """ | ||||
|         ); | ||||
|  | ||||
|         var application = new CliApplicationBuilder() | ||||
|             .AddCommand(commandType) | ||||
|             .UseConsole(FakeConsole) | ||||
|             .Build(); | ||||
|  | ||||
|         // Act | ||||
|         var exitCode = await application.RunAsync(["--help"], new Dictionary<string, string>()); | ||||
|  | ||||
|         // Assert | ||||
|         exitCode.Should().Be(0); | ||||
|  | ||||
|         var stdOut = FakeConsole.ReadOutputString(); | ||||
|  | ||||
|         stdOut | ||||
|             .Should() | ||||
|             .ContainAllInOrder( | ||||
|                 "OPTIONS", | ||||
|                 "--foo", | ||||
|                 "Default:", | ||||
|                 "42", | ||||
|                 "--bar", | ||||
|                 "Default:", | ||||
|                 "hello", | ||||
|                 "--baz", | ||||
|                 "Default:", | ||||
|                 "one", | ||||
|                 "two", | ||||
|                 "three", | ||||
|                 "--qwe", | ||||
|                 "Default:", | ||||
|                 "True", | ||||
|                 "--qop", | ||||
|                 "Default:", | ||||
|                 "1337", | ||||
|                 "--zor", | ||||
|                 "Default:", | ||||
|                 "02:03:00", | ||||
|                 "--lol", | ||||
|                 "Default:", | ||||
|                 "Two" | ||||
|             ); | ||||
|  | ||||
|         stdOut.Should().NotContain("not printed"); | ||||
|     } | ||||
|  | ||||
|     [Fact] | ||||
|     public async Task I_can_request_the_help_text_to_see_the_list_of_all_immediate_child_commands() | ||||
|     { | ||||
|         // Arrange | ||||
|         var commandTypes = DynamicCommandBuilder.CompileMany( | ||||
|             // lang=csharp | ||||
|             """ | ||||
|             [Command("cmd1", Description = "Description of one command.")] | ||||
|             public class FirstCommand : ICommand | ||||
|             { | ||||
|                 public ValueTask ExecuteAsync(IConsole console) => default; | ||||
|             } | ||||
|  | ||||
|             [Command("cmd2", Description = "Description of another command.")] | ||||
|             public class SecondCommand : ICommand | ||||
|             { | ||||
|                 public ValueTask ExecuteAsync(IConsole console) => default; | ||||
|             } | ||||
|  | ||||
|             [Command("cmd2 child", Description = "Description of another command's child command.")] | ||||
|             public class SecondCommandChildCommand : ICommand | ||||
|             { | ||||
|                 public ValueTask ExecuteAsync(IConsole console) => default; | ||||
|             } | ||||
|  | ||||
|             [Command("cmd3 child", Description = "Description of an orphaned command.")] | ||||
|             public class ThirdCommandChildCommand : ICommand | ||||
|             { | ||||
|                 public ValueTask ExecuteAsync(IConsole console) => default; | ||||
|             } | ||||
|             """ | ||||
|         ); | ||||
|  | ||||
|         var application = new CliApplicationBuilder() | ||||
|             .AddCommands(commandTypes) | ||||
|             .UseConsole(FakeConsole) | ||||
|             .Build(); | ||||
|  | ||||
|         // Act | ||||
|         var exitCode = await application.RunAsync(["--help"], new Dictionary<string, string>()); | ||||
|  | ||||
|         // Assert | ||||
|         exitCode.Should().Be(0); | ||||
|  | ||||
|         var stdOut = FakeConsole.ReadOutputString(); | ||||
|  | ||||
|         stdOut | ||||
|             .Should() | ||||
|             .ContainAllInOrder( | ||||
|                 "COMMANDS", | ||||
|                 "cmd1", | ||||
|                 "Description of one command.", | ||||
|                 "cmd2", | ||||
|                 "Description of another command.", | ||||
|                 // `cmd2 child` will appear as an immediate command because it does not | ||||
|                 // have a more specific parent. | ||||
|                 "cmd3 child", | ||||
|                 "Description of an orphaned command." | ||||
|             ); | ||||
|  | ||||
|         // `cmd2 child` will still appear in the list of `cmd2` subcommands, | ||||
|         // but its description will not be visible. | ||||
|         stdOut.Should().NotContain("Description of another command's child command."); | ||||
|     } | ||||
|  | ||||
|     [Fact] | ||||
|     public async Task I_can_request_the_help_text_to_see_the_list_of_all_immediate_grand_child_commands() | ||||
|     { | ||||
|         // Arrange | ||||
|         var commandTypes = DynamicCommandBuilder.CompileMany( | ||||
|             // lang=csharp | ||||
|             """ | ||||
|             [Command("cmd1")] | ||||
|             public class FirstCommand : ICommand | ||||
|             { | ||||
|                 public ValueTask ExecuteAsync(IConsole console) => default; | ||||
|             } | ||||
|  | ||||
|             [Command("cmd1 child1")] | ||||
|             public class FirstCommandFirstChildCommand : ICommand | ||||
|             { | ||||
|                 public ValueTask ExecuteAsync(IConsole console) => default; | ||||
|             } | ||||
|  | ||||
|             [Command("cmd2")] | ||||
|             public class SecondCommand : ICommand | ||||
|             { | ||||
|                 public ValueTask ExecuteAsync(IConsole console) => default; | ||||
|             } | ||||
|  | ||||
|             [Command("cmd2 child11")] | ||||
|             public class SecondCommandFirstChildCommand : ICommand | ||||
|             { | ||||
|                 public ValueTask ExecuteAsync(IConsole console) => default; | ||||
|             } | ||||
|  | ||||
|             [Command("cmd2 child2")] | ||||
|             public class SecondCommandSecondChildCommand : ICommand | ||||
|             { | ||||
|                 public ValueTask ExecuteAsync(IConsole console) => default; | ||||
|             } | ||||
|             """ | ||||
|         ); | ||||
|  | ||||
|         var application = new CliApplicationBuilder() | ||||
|             .AddCommands(commandTypes) | ||||
|             .UseConsole(FakeConsole) | ||||
|             .Build(); | ||||
|  | ||||
|         // Act | ||||
|         var exitCode = await application.RunAsync(["--help"], new Dictionary<string, string>()); | ||||
|  | ||||
|         // Assert | ||||
|         exitCode.Should().Be(0); | ||||
|  | ||||
|         var stdOut = FakeConsole.ReadOutputString(); | ||||
|         stdOut | ||||
|             .Should() | ||||
|             .ContainAllInOrder( | ||||
|                 "COMMANDS", | ||||
|                 "cmd1", | ||||
|                 "Subcommands:", | ||||
|                 "cmd1 child1", | ||||
|                 "cmd2", | ||||
|                 "Subcommands:", | ||||
|                 "cmd2 child1", | ||||
|                 "cmd2 child2" | ||||
|             ); | ||||
|     } | ||||
|  | ||||
|     [Fact] | ||||
|     public async Task I_can_request_the_version_text_by_running_the_application_with_the_version_option() | ||||
|     { | ||||
|         // Arrange | ||||
|         var application = new CliApplicationBuilder() | ||||
|             .AddCommand<NoOpCommand>() | ||||
|             .SetVersion("v6.9") | ||||
|             .UseConsole(FakeConsole) | ||||
|             .Build(); | ||||
|  | ||||
|         // Act | ||||
|         var exitCode = await application.RunAsync(["--version"], new Dictionary<string, string>()); | ||||
|  | ||||
|         // Assert | ||||
|         exitCode.Should().Be(0); | ||||
|  | ||||
|         var stdOut = FakeConsole.ReadOutputString(); | ||||
|         stdOut.Trim().Should().Be("v6.9"); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										762
									
								
								CliFx.Tests/OptionBindingSpecs.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										762
									
								
								CliFx.Tests/OptionBindingSpecs.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,762 @@ | ||||
| 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_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"); | ||||
|     } | ||||
| } | ||||
| @@ -1,37 +0,0 @@ | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
| using System.Linq; | ||||
| using CliFx.Models; | ||||
| using CliFx.Services; | ||||
| using CliFx.Tests.TestCommands; | ||||
| using FluentAssertions; | ||||
| using NUnit.Framework; | ||||
|  | ||||
| namespace CliFx.Tests.Services | ||||
| { | ||||
|     [TestFixture] | ||||
|     public class CommandFactoryTests | ||||
|     { | ||||
|         private static CommandSchema GetCommandSchema(Type commandType) => | ||||
|             new CommandSchemaResolver().GetCommandSchemas(new[] {commandType}).Single(); | ||||
|  | ||||
|         private static IEnumerable<TestCaseData> GetTestCases_CreateCommand() | ||||
|         { | ||||
|             yield return new TestCaseData(GetCommandSchema(typeof(HelloWorldDefaultCommand))); | ||||
|         } | ||||
|  | ||||
|         [Test] | ||||
|         [TestCaseSource(nameof(GetTestCases_CreateCommand))] | ||||
|         public void CreateCommand_Test(CommandSchema commandSchema) | ||||
|         { | ||||
|             // Arrange | ||||
|             var factory = new CommandFactory(); | ||||
|  | ||||
|             // Act | ||||
|             var command = factory.CreateCommand(commandSchema); | ||||
|  | ||||
|             // Assert | ||||
|             command.Should().BeOfType(commandSchema.Type); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -1,173 +0,0 @@ | ||||
| using FluentAssertions; | ||||
| using NUnit.Framework; | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
| using System.Linq; | ||||
| using CliFx.Exceptions; | ||||
| using CliFx.Models; | ||||
| using CliFx.Services; | ||||
| using CliFx.Tests.TestCommands; | ||||
| using CliFx.Tests.Stubs; | ||||
|  | ||||
| namespace CliFx.Tests.Services | ||||
| { | ||||
|     [TestFixture] | ||||
|     public class CommandInitializerTests | ||||
|     { | ||||
|         private static CommandSchema GetCommandSchema(Type commandType) => | ||||
|             new CommandSchemaResolver().GetCommandSchemas(new[] { commandType }).Single(); | ||||
|  | ||||
|         private static IEnumerable<TestCaseData> GetTestCases_InitializeCommand() | ||||
|         { | ||||
|             yield return new TestCaseData( | ||||
|                 new DivideCommand(), | ||||
|                 GetCommandSchema(typeof(DivideCommand)), | ||||
|                 new CommandInput("div", new[] | ||||
|                 { | ||||
|                     new CommandOptionInput("dividend", "13"), | ||||
|                     new CommandOptionInput("divisor", "8") | ||||
|                 }), | ||||
|                 new DivideCommand { Dividend = 13, Divisor = 8 } | ||||
|             ); | ||||
|  | ||||
|             yield return new TestCaseData( | ||||
|                 new DivideCommand(), | ||||
|                 GetCommandSchema(typeof(DivideCommand)), | ||||
|                 new CommandInput("div", new[] | ||||
|                 { | ||||
|                     new CommandOptionInput("dividend", "13"), | ||||
|                     new CommandOptionInput("d", "8") | ||||
|                 }), | ||||
|                 new DivideCommand { Dividend = 13, Divisor = 8 } | ||||
|             ); | ||||
|  | ||||
|             yield return new TestCaseData( | ||||
|                 new DivideCommand(), | ||||
|                 GetCommandSchema(typeof(DivideCommand)), | ||||
|                 new CommandInput("div", new[] | ||||
|                 { | ||||
|                     new CommandOptionInput("D", "13"), | ||||
|                     new CommandOptionInput("d", "8") | ||||
|                 }), | ||||
|                 new DivideCommand { Dividend = 13, Divisor = 8 } | ||||
|             ); | ||||
|  | ||||
|             yield return new TestCaseData( | ||||
|                 new ConcatCommand(), | ||||
|                 GetCommandSchema(typeof(ConcatCommand)), | ||||
|                 new CommandInput("concat", new[] | ||||
|                 { | ||||
|                     new CommandOptionInput("i", new[] {"foo", " ", "bar"}) | ||||
|                 }), | ||||
|                 new ConcatCommand { Inputs = new[] { "foo", " ", "bar" } } | ||||
|             ); | ||||
|  | ||||
|             yield return new TestCaseData( | ||||
|                 new ConcatCommand(), | ||||
|                 GetCommandSchema(typeof(ConcatCommand)), | ||||
|                 new CommandInput("concat", new[] | ||||
|                 { | ||||
|                     new CommandOptionInput("i", new[] {"foo", "bar"}), | ||||
|                     new CommandOptionInput("s", " ") | ||||
|                 }), | ||||
|                 new ConcatCommand { Inputs = new[] { "foo", "bar" }, Separator = " " } | ||||
|             ); | ||||
|  | ||||
|             //Will read a value from environment variables because none is supplied via CommandInput | ||||
|             yield return new TestCaseData( | ||||
|                 new EnvironmentVariableCommand(), | ||||
|                 GetCommandSchema(typeof(EnvironmentVariableCommand)), | ||||
|                 new CommandInput(null, new CommandOptionInput[0], EnvironmentVariablesProviderStub.EnvironmentVariables), | ||||
|                 new EnvironmentVariableCommand { Option = "A" } | ||||
|             ); | ||||
|  | ||||
|             //Will read multiple values from environment variables because none is supplied via CommandInput | ||||
|             yield return new TestCaseData( | ||||
|                 new EnvironmentVariableWithMultipleValuesCommand(), | ||||
|                 GetCommandSchema(typeof(EnvironmentVariableWithMultipleValuesCommand)), | ||||
|                 new CommandInput(null, new CommandOptionInput[0], EnvironmentVariablesProviderStub.EnvironmentVariables), | ||||
|                 new EnvironmentVariableWithMultipleValuesCommand { Option = new[] { "A", "B", "C" } } | ||||
|             ); | ||||
|  | ||||
|             //Will not read a value from environment variables because one is supplied via CommandInput | ||||
|             yield return new TestCaseData( | ||||
|                 new EnvironmentVariableCommand(), | ||||
|                 GetCommandSchema(typeof(EnvironmentVariableCommand)), | ||||
|                 new CommandInput(null, new[] | ||||
|                 { | ||||
|                     new CommandOptionInput("opt", new[] { "X" }) | ||||
|                 }, | ||||
|                 EnvironmentVariablesProviderStub.EnvironmentVariables), | ||||
|                 new EnvironmentVariableCommand { Option = "X" } | ||||
|             ); | ||||
|  | ||||
|             //Will not split environment variable values because underlying property is not a collection | ||||
|             yield return new TestCaseData( | ||||
|                 new EnvironmentVariableWithoutCollectionPropertyCommand(), | ||||
|                 GetCommandSchema(typeof(EnvironmentVariableWithoutCollectionPropertyCommand)), | ||||
|                 new CommandInput(null, new CommandOptionInput[0], EnvironmentVariablesProviderStub.EnvironmentVariables), | ||||
|                 new EnvironmentVariableWithoutCollectionPropertyCommand { Option = "A;B;C;" } | ||||
|             ); | ||||
|         } | ||||
|  | ||||
|         private static IEnumerable<TestCaseData> GetTestCases_InitializeCommand_Negative() | ||||
|         { | ||||
|             yield return new TestCaseData( | ||||
|                 new DivideCommand(), | ||||
|                 GetCommandSchema(typeof(DivideCommand)), | ||||
|                 new CommandInput("div") | ||||
|             ); | ||||
|  | ||||
|             yield return new TestCaseData( | ||||
|                 new DivideCommand(), | ||||
|                 GetCommandSchema(typeof(DivideCommand)), | ||||
|                 new CommandInput("div", new[] | ||||
|                 { | ||||
|                     new CommandOptionInput("D", "13") | ||||
|                 }) | ||||
|             ); | ||||
|  | ||||
|             yield return new TestCaseData( | ||||
|                 new ConcatCommand(), | ||||
|                 GetCommandSchema(typeof(ConcatCommand)), | ||||
|                 new CommandInput("concat") | ||||
|             ); | ||||
|  | ||||
|             yield return new TestCaseData( | ||||
|                 new ConcatCommand(), | ||||
|                 GetCommandSchema(typeof(ConcatCommand)), | ||||
|                 new CommandInput("concat", new[] | ||||
|                 { | ||||
|                     new CommandOptionInput("s", "_") | ||||
|                 }) | ||||
|             ); | ||||
|         } | ||||
|  | ||||
|         [Test] | ||||
|         [TestCaseSource(nameof(GetTestCases_InitializeCommand))] | ||||
|         public void InitializeCommand_Test(ICommand command, CommandSchema commandSchema, CommandInput commandInput, | ||||
|             ICommand expectedCommand) | ||||
|         { | ||||
|             // Arrange | ||||
|             var initializer = new CommandInitializer(); | ||||
|  | ||||
|             // Act | ||||
|             initializer.InitializeCommand(command, commandSchema, commandInput); | ||||
|  | ||||
|             // Assert | ||||
|             command.Should().BeEquivalentTo(expectedCommand, o => o.RespectingRuntimeTypes()); | ||||
|         } | ||||
|  | ||||
|         [Test] | ||||
|         [TestCaseSource(nameof(GetTestCases_InitializeCommand_Negative))] | ||||
|         public void InitializeCommand_Negative_Test(ICommand command, CommandSchema commandSchema, CommandInput commandInput) | ||||
|         { | ||||
|             // Arrange | ||||
|             var initializer = new CommandInitializer(); | ||||
|  | ||||
|             // Act & Assert | ||||
|             initializer.Invoking(i => i.InitializeCommand(command, commandSchema, commandInput)) | ||||
|                 .Should().ThrowExactly<CliFxException>(); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -1,255 +0,0 @@ | ||||
| using FluentAssertions; | ||||
| using NUnit.Framework; | ||||
| using System.Collections.Generic; | ||||
| using CliFx.Models; | ||||
| using CliFx.Services; | ||||
| using CliFx.Tests.Stubs; | ||||
|  | ||||
| namespace CliFx.Tests.Services | ||||
| { | ||||
|     [TestFixture] | ||||
|     public class CommandInputParserTests | ||||
|     { | ||||
|         private static IEnumerable<TestCaseData> GetTestCases_ParseCommandInput() | ||||
|         { | ||||
|             yield return new TestCaseData(new string[0], CommandInput.Empty, new EmptyEnvironmentVariablesProviderStub()); | ||||
|  | ||||
|             yield return new TestCaseData( | ||||
|                 new[] { "--option", "value" }, | ||||
|                 new CommandInput(new[] | ||||
|                 { | ||||
|                     new CommandOptionInput("option", "value") | ||||
|                 }), | ||||
|                 new EmptyEnvironmentVariablesProviderStub() | ||||
|             ); | ||||
|  | ||||
|             yield return new TestCaseData( | ||||
|                 new[] { "--option1", "value1", "--option2", "value2" }, | ||||
|                 new CommandInput(new[] | ||||
|                 { | ||||
|                     new CommandOptionInput("option1", "value1"), | ||||
|                     new CommandOptionInput("option2", "value2") | ||||
|                 }), | ||||
|                 new EmptyEnvironmentVariablesProviderStub() | ||||
|             ); | ||||
|  | ||||
|             yield return new TestCaseData( | ||||
|                 new[] { "--option", "value1", "value2" }, | ||||
|                 new CommandInput(new[] | ||||
|                 { | ||||
|                     new CommandOptionInput("option", new[] {"value1", "value2"}) | ||||
|                 }), | ||||
|                 new EmptyEnvironmentVariablesProviderStub() | ||||
|             ); | ||||
|  | ||||
|             yield return new TestCaseData( | ||||
|                 new[] { "--option", "value1", "--option", "value2" }, | ||||
|                 new CommandInput(new[] | ||||
|                 { | ||||
|                     new CommandOptionInput("option", new[] {"value1", "value2"}) | ||||
|                 }), | ||||
|                 new EmptyEnvironmentVariablesProviderStub() | ||||
|             ); | ||||
|  | ||||
|             yield return new TestCaseData( | ||||
|                 new[] { "-a", "value" }, | ||||
|                 new CommandInput(new[] | ||||
|                 { | ||||
|                     new CommandOptionInput("a", "value") | ||||
|                 }), | ||||
|                 new EmptyEnvironmentVariablesProviderStub() | ||||
|             ); | ||||
|  | ||||
|             yield return new TestCaseData( | ||||
|                 new[] { "-a", "value1", "-b", "value2" }, | ||||
|                 new CommandInput(new[] | ||||
|                 { | ||||
|                     new CommandOptionInput("a", "value1"), | ||||
|                     new CommandOptionInput("b", "value2") | ||||
|                 }), | ||||
|                 new EmptyEnvironmentVariablesProviderStub() | ||||
|             ); | ||||
|  | ||||
|             yield return new TestCaseData( | ||||
|                 new[] { "-a", "value1", "value2" }, | ||||
|                 new CommandInput(new[] | ||||
|                 { | ||||
|                     new CommandOptionInput("a", new[] {"value1", "value2"}) | ||||
|                 }), | ||||
|                 new EmptyEnvironmentVariablesProviderStub() | ||||
|             ); | ||||
|  | ||||
|             yield return new TestCaseData( | ||||
|                 new[] { "-a", "value1", "-a", "value2" }, | ||||
|                 new CommandInput(new[] | ||||
|                 { | ||||
|                     new CommandOptionInput("a", new[] {"value1", "value2"}) | ||||
|                 }), | ||||
|                 new EmptyEnvironmentVariablesProviderStub() | ||||
|             ); | ||||
|  | ||||
|             yield return new TestCaseData( | ||||
|                 new[] { "--option1", "value1", "-b", "value2" }, | ||||
|                 new CommandInput(new[] | ||||
|                 { | ||||
|                     new CommandOptionInput("option1", "value1"), | ||||
|                     new CommandOptionInput("b", "value2") | ||||
|                 }), | ||||
|                 new EmptyEnvironmentVariablesProviderStub() | ||||
|             ); | ||||
|  | ||||
|             yield return new TestCaseData( | ||||
|                 new[] { "--switch" }, | ||||
|                 new CommandInput(new[] | ||||
|                 { | ||||
|                     new CommandOptionInput("switch") | ||||
|                 }), | ||||
|                 new EmptyEnvironmentVariablesProviderStub() | ||||
|             ); | ||||
|  | ||||
|             yield return new TestCaseData( | ||||
|                 new[] { "--switch1", "--switch2" }, | ||||
|                 new CommandInput(new[] | ||||
|                 { | ||||
|                     new CommandOptionInput("switch1"), | ||||
|                     new CommandOptionInput("switch2") | ||||
|                 }), | ||||
|                 new EmptyEnvironmentVariablesProviderStub() | ||||
|             ); | ||||
|  | ||||
|             yield return new TestCaseData( | ||||
|                 new[] { "-s" }, | ||||
|                 new CommandInput(new[] | ||||
|                 { | ||||
|                     new CommandOptionInput("s") | ||||
|                 }), | ||||
|                 new EmptyEnvironmentVariablesProviderStub() | ||||
|             ); | ||||
|  | ||||
|             yield return new TestCaseData( | ||||
|                 new[] { "-a", "-b" }, | ||||
|                 new CommandInput(new[] | ||||
|                 { | ||||
|                     new CommandOptionInput("a"), | ||||
|                     new CommandOptionInput("b") | ||||
|                 }), | ||||
|                 new EmptyEnvironmentVariablesProviderStub() | ||||
|             ); | ||||
|  | ||||
|             yield return new TestCaseData( | ||||
|                 new[] { "-ab" }, | ||||
|                 new CommandInput(new[] | ||||
|                 { | ||||
|                     new CommandOptionInput("a"), | ||||
|                     new CommandOptionInput("b") | ||||
|                 }), | ||||
|                 new EmptyEnvironmentVariablesProviderStub() | ||||
|             ); | ||||
|  | ||||
|             yield return new TestCaseData( | ||||
|                 new[] { "-ab", "value" }, | ||||
|                 new CommandInput(new[] | ||||
|                 { | ||||
|                     new CommandOptionInput("a"), | ||||
|                     new CommandOptionInput("b", "value") | ||||
|                 }), | ||||
|                 new EmptyEnvironmentVariablesProviderStub() | ||||
|             ); | ||||
|  | ||||
|             yield return new TestCaseData( | ||||
|                 new[] { "command" }, | ||||
|                 new CommandInput("command"), | ||||
|                 new EmptyEnvironmentVariablesProviderStub() | ||||
|             ); | ||||
|  | ||||
|             yield return new TestCaseData( | ||||
|                 new[] { "command", "--option", "value" }, | ||||
|                 new CommandInput("command", new[] | ||||
|                 { | ||||
|                     new CommandOptionInput("option", "value") | ||||
|                 }), | ||||
|                 new EmptyEnvironmentVariablesProviderStub() | ||||
|             ); | ||||
|  | ||||
|             yield return new TestCaseData( | ||||
|                 new[] { "long", "command", "name" }, | ||||
|                 new CommandInput("long command name"), | ||||
|                 new EmptyEnvironmentVariablesProviderStub() | ||||
|             ); | ||||
|  | ||||
|             yield return new TestCaseData( | ||||
|                 new[] { "long", "command", "name", "--option", "value" }, | ||||
|                 new CommandInput("long command name", new[] | ||||
|                 { | ||||
|                     new CommandOptionInput("option", "value") | ||||
|                 }), | ||||
|                 new EmptyEnvironmentVariablesProviderStub() | ||||
|             ); | ||||
|  | ||||
|             yield return new TestCaseData( | ||||
|                 new[] { "[debug]" }, | ||||
|                 new CommandInput(null, | ||||
|                     new[] { "debug" }, | ||||
|                     new CommandOptionInput[0]), | ||||
|                 new EmptyEnvironmentVariablesProviderStub() | ||||
|             ); | ||||
|  | ||||
|             yield return new TestCaseData( | ||||
|                 new[] { "[debug]", "[preview]" }, | ||||
|                 new CommandInput(null, | ||||
|                     new[] { "debug", "preview" }, | ||||
|                     new CommandOptionInput[0]), | ||||
|                 new EmptyEnvironmentVariablesProviderStub() | ||||
|             ); | ||||
|  | ||||
|             yield return new TestCaseData( | ||||
|                 new[] { "[debug]", "[preview]", "-o", "value" }, | ||||
|                 new CommandInput(null, | ||||
|                     new[] { "debug", "preview" }, | ||||
|                     new[] | ||||
|                     { | ||||
|                         new CommandOptionInput("o", "value") | ||||
|                     }), | ||||
|                 new EmptyEnvironmentVariablesProviderStub() | ||||
|             ); | ||||
|  | ||||
|             yield return new TestCaseData( | ||||
|                 new[] { "command", "[debug]", "[preview]", "-o", "value" }, | ||||
|                 new CommandInput("command", | ||||
|                     new[] { "debug", "preview" }, | ||||
|                     new[] | ||||
|                     { | ||||
|                         new CommandOptionInput("o", "value") | ||||
|                     }), | ||||
|                 new EmptyEnvironmentVariablesProviderStub() | ||||
|             ); | ||||
|  | ||||
|             yield return new TestCaseData( | ||||
|                 new[] { "command", "[debug]", "[preview]", "-o", "value" }, | ||||
|                 new CommandInput("command", | ||||
|                     new[] { "debug", "preview" }, | ||||
|                     new[] | ||||
|                     { | ||||
|                         new CommandOptionInput("o", "value") | ||||
|                     }, | ||||
|                     EnvironmentVariablesProviderStub.EnvironmentVariables), | ||||
|                 new EnvironmentVariablesProviderStub() | ||||
|             ); | ||||
|         } | ||||
|  | ||||
|         [Test] | ||||
|         [TestCaseSource(nameof(GetTestCases_ParseCommandInput))] | ||||
|         public void ParseCommandInput_Test(IReadOnlyList<string> commandLineArguments, | ||||
|             CommandInput expectedCommandInput, IEnvironmentVariablesProvider environmentVariablesProvider) | ||||
|         { | ||||
|             // Arrange | ||||
|             var parser = new CommandInputParser(environmentVariablesProvider); | ||||
|  | ||||
|             // Act | ||||
|             var commandInput = parser.ParseCommandInput(commandLineArguments); | ||||
|  | ||||
|             // Assert | ||||
|             commandInput.Should().BeEquivalentTo(expectedCommandInput); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -1,323 +0,0 @@ | ||||
| using System; | ||||
| using System.Collections; | ||||
| using System.Collections.Generic; | ||||
| using System.Globalization; | ||||
| using CliFx.Exceptions; | ||||
| using CliFx.Models; | ||||
| using CliFx.Services; | ||||
| using CliFx.Tests.TestCustomTypes; | ||||
| using FluentAssertions; | ||||
| using NUnit.Framework; | ||||
|  | ||||
| namespace CliFx.Tests.Services | ||||
| { | ||||
|     [TestFixture] | ||||
|     public class CommandOptionInputConverterTests | ||||
|     { | ||||
|         private static IEnumerable<TestCaseData> GetTestCases_ConvertOptionInput() | ||||
|         { | ||||
|             yield return new TestCaseData( | ||||
|                 new CommandOptionInput("option", "value"), | ||||
|                 typeof(string), | ||||
|                 "value" | ||||
|             ); | ||||
|  | ||||
|             yield return new TestCaseData( | ||||
|                 new CommandOptionInput("option", "value"), | ||||
|                 typeof(object), | ||||
|                 "value" | ||||
|             ); | ||||
|  | ||||
|             yield return new TestCaseData( | ||||
|                 new CommandOptionInput("option", "true"), | ||||
|                 typeof(bool), | ||||
|                 true | ||||
|             ); | ||||
|  | ||||
|             yield return new TestCaseData( | ||||
|                 new CommandOptionInput("option", "false"), | ||||
|                 typeof(bool), | ||||
|                 false | ||||
|             ); | ||||
|  | ||||
|             yield return new TestCaseData( | ||||
|                 new CommandOptionInput("option"), | ||||
|                 typeof(bool), | ||||
|                 true | ||||
|             ); | ||||
|  | ||||
|             yield return new TestCaseData( | ||||
|                 new CommandOptionInput("option", "a"), | ||||
|                 typeof(char), | ||||
|                 'a' | ||||
|             ); | ||||
|  | ||||
|             yield return new TestCaseData( | ||||
|                 new CommandOptionInput("option", "15"), | ||||
|                 typeof(sbyte), | ||||
|                 (sbyte) 15 | ||||
|             ); | ||||
|  | ||||
|             yield return new TestCaseData( | ||||
|                 new CommandOptionInput("option", "15"), | ||||
|                 typeof(byte), | ||||
|                 (byte) 15 | ||||
|             ); | ||||
|  | ||||
|             yield return new TestCaseData( | ||||
|                 new CommandOptionInput("option", "15"), | ||||
|                 typeof(short), | ||||
|                 (short) 15 | ||||
|             ); | ||||
|  | ||||
|             yield return new TestCaseData( | ||||
|                 new CommandOptionInput("option", "15"), | ||||
|                 typeof(ushort), | ||||
|                 (ushort) 15 | ||||
|             ); | ||||
|  | ||||
|             yield return new TestCaseData( | ||||
|                 new CommandOptionInput("option", "123"), | ||||
|                 typeof(int), | ||||
|                 123 | ||||
|             ); | ||||
|  | ||||
|             yield return new TestCaseData( | ||||
|                 new CommandOptionInput("option", "123"), | ||||
|                 typeof(uint), | ||||
|                 123u | ||||
|             ); | ||||
|  | ||||
|             yield return new TestCaseData( | ||||
|                 new CommandOptionInput("option", "123"), | ||||
|                 typeof(long), | ||||
|                 123L | ||||
|             ); | ||||
|  | ||||
|             yield return new TestCaseData( | ||||
|                 new CommandOptionInput("option", "123"), | ||||
|                 typeof(ulong), | ||||
|                 123UL | ||||
|             ); | ||||
|  | ||||
|             yield return new TestCaseData( | ||||
|                 new CommandOptionInput("option", "123.45"), | ||||
|                 typeof(float), | ||||
|                 123.45f | ||||
|             ); | ||||
|  | ||||
|             yield return new TestCaseData( | ||||
|                 new CommandOptionInput("option", "123.45"), | ||||
|                 typeof(double), | ||||
|                 123.45 | ||||
|             ); | ||||
|  | ||||
|             yield return new TestCaseData( | ||||
|                 new CommandOptionInput("option", "123.45"), | ||||
|                 typeof(decimal), | ||||
|                 123.45m | ||||
|             ); | ||||
|  | ||||
|             yield return new TestCaseData( | ||||
|                 new CommandOptionInput("option", "28 Apr 1995"), | ||||
|                 typeof(DateTime), | ||||
|                 new DateTime(1995, 04, 28) | ||||
|             ); | ||||
|  | ||||
|             yield return new TestCaseData( | ||||
|                 new CommandOptionInput("option", "28 Apr 1995"), | ||||
|                 typeof(DateTimeOffset), | ||||
|                 new DateTimeOffset(new DateTime(1995, 04, 28)) | ||||
|             ); | ||||
|  | ||||
|             yield return new TestCaseData( | ||||
|                 new CommandOptionInput("option", "00:14:59"), | ||||
|                 typeof(TimeSpan), | ||||
|                 new TimeSpan(00, 14, 59) | ||||
|             ); | ||||
|  | ||||
|             yield return new TestCaseData( | ||||
|                 new CommandOptionInput("option", "value2"), | ||||
|                 typeof(TestEnum), | ||||
|                 TestEnum.Value2 | ||||
|             ); | ||||
|  | ||||
|             yield return new TestCaseData( | ||||
|                 new CommandOptionInput("option", "666"), | ||||
|                 typeof(int?), | ||||
|                 666 | ||||
|             ); | ||||
|  | ||||
|             yield return new TestCaseData( | ||||
|                 new CommandOptionInput("option"), | ||||
|                 typeof(int?), | ||||
|                 null | ||||
|             ); | ||||
|  | ||||
|             yield return new TestCaseData( | ||||
|                 new CommandOptionInput("option", "value3"), | ||||
|                 typeof(TestEnum?), | ||||
|                 TestEnum.Value3 | ||||
|             ); | ||||
|  | ||||
|             yield return new TestCaseData( | ||||
|                 new CommandOptionInput("option"), | ||||
|                 typeof(TestEnum?), | ||||
|                 null | ||||
|             ); | ||||
|  | ||||
|             yield return new TestCaseData( | ||||
|                 new CommandOptionInput("option", "01:00:00"), | ||||
|                 typeof(TimeSpan?), | ||||
|                 new TimeSpan(01, 00, 00) | ||||
|             ); | ||||
|  | ||||
|             yield return new TestCaseData( | ||||
|                 new CommandOptionInput("option"), | ||||
|                 typeof(TimeSpan?), | ||||
|                 null | ||||
|             ); | ||||
|  | ||||
|             yield return new TestCaseData( | ||||
|                 new CommandOptionInput("option", "value"), | ||||
|                 typeof(TestStringConstructable), | ||||
|                 new TestStringConstructable("value") | ||||
|             ); | ||||
|  | ||||
|             yield return new TestCaseData( | ||||
|                 new CommandOptionInput("option", "value"), | ||||
|                 typeof(TestStringParseable), | ||||
|                 TestStringParseable.Parse("value") | ||||
|             ); | ||||
|  | ||||
|             yield return new TestCaseData( | ||||
|                 new CommandOptionInput("option", "value"), | ||||
|                 typeof(TestStringParseableWithFormatProvider), | ||||
|                 TestStringParseableWithFormatProvider.Parse("value", CultureInfo.InvariantCulture) | ||||
|             ); | ||||
|  | ||||
|             yield return new TestCaseData( | ||||
|                 new CommandOptionInput("option", new[] {"value1", "value2"}), | ||||
|                 typeof(string[]), | ||||
|                 new[] {"value1", "value2"} | ||||
|             ); | ||||
|  | ||||
|             yield return new TestCaseData( | ||||
|                 new CommandOptionInput("option", new[] {"value1", "value2"}), | ||||
|                 typeof(object[]), | ||||
|                 new[] {"value1", "value2"} | ||||
|             ); | ||||
|  | ||||
|             yield return new TestCaseData( | ||||
|                 new CommandOptionInput("option", new[] {"47", "69"}), | ||||
|                 typeof(int[]), | ||||
|                 new[] {47, 69} | ||||
|             ); | ||||
|  | ||||
|             yield return new TestCaseData( | ||||
|                 new CommandOptionInput("option", new[] {"47"}), | ||||
|                 typeof(int[]), | ||||
|                 new[] {47} | ||||
|             ); | ||||
|  | ||||
|             yield return new TestCaseData( | ||||
|                 new CommandOptionInput("option", new[] {"value1", "value3"}), | ||||
|                 typeof(TestEnum[]), | ||||
|                 new[] {TestEnum.Value1, TestEnum.Value3} | ||||
|             ); | ||||
|  | ||||
|             yield return new TestCaseData( | ||||
|                 new CommandOptionInput("option", new[] {"1337", "2441"}), | ||||
|                 typeof(int?[]), | ||||
|                 new int?[] {1337, 2441} | ||||
|             ); | ||||
|  | ||||
|             yield return new TestCaseData( | ||||
|                 new CommandOptionInput("option", new[] {"value1", "value2"}), | ||||
|                 typeof(TestStringConstructable[]), | ||||
|                 new[] {new TestStringConstructable("value1"), new TestStringConstructable("value2")} | ||||
|             ); | ||||
|  | ||||
|             yield return new TestCaseData( | ||||
|                 new CommandOptionInput("option", new[] {"value1", "value2"}), | ||||
|                 typeof(IEnumerable), | ||||
|                 new[] {"value1", "value2"} | ||||
|             ); | ||||
|  | ||||
|             yield return new TestCaseData( | ||||
|                 new CommandOptionInput("option", new[] {"value1", "value2"}), | ||||
|                 typeof(IEnumerable<string>), | ||||
|                 new[] {"value1", "value2"} | ||||
|             ); | ||||
|  | ||||
|             yield return new TestCaseData( | ||||
|                 new CommandOptionInput("option", new[] {"value1", "value2"}), | ||||
|                 typeof(IReadOnlyList<string>), | ||||
|                 new[] {"value1", "value2"} | ||||
|             ); | ||||
|  | ||||
|             yield return new TestCaseData( | ||||
|                 new CommandOptionInput("option", new[] {"value1", "value2"}), | ||||
|                 typeof(List<string>), | ||||
|                 new List<string> {"value1", "value2"} | ||||
|             ); | ||||
|  | ||||
|             yield return new TestCaseData( | ||||
|                 new CommandOptionInput("option", new[] {"value1", "value2"}), | ||||
|                 typeof(HashSet<string>), | ||||
|                 new HashSet<string> {"value1", "value2"} | ||||
|             ); | ||||
|         } | ||||
|  | ||||
|         private static IEnumerable<TestCaseData> GetTestCases_ConvertOptionInput_Negative() | ||||
|         { | ||||
|             yield return new TestCaseData( | ||||
|                 new CommandOptionInput("option", "1234.5"), | ||||
|                 typeof(int) | ||||
|             ); | ||||
|  | ||||
|             yield return new TestCaseData( | ||||
|                 new CommandOptionInput("option", new[] {"123", "456"}), | ||||
|                 typeof(int) | ||||
|             ); | ||||
|  | ||||
|             yield return new TestCaseData( | ||||
|                 new CommandOptionInput("option"), | ||||
|                 typeof(int) | ||||
|             ); | ||||
|  | ||||
|             yield return new TestCaseData( | ||||
|                 new CommandOptionInput("option", "123"), | ||||
|                 typeof(TestNonStringParseable) | ||||
|             ); | ||||
|         } | ||||
|  | ||||
|         [Test] | ||||
|         [TestCaseSource(nameof(GetTestCases_ConvertOptionInput))] | ||||
|         public void ConvertOptionInput_Test(CommandOptionInput optionInput, Type targetType, | ||||
|             object expectedConvertedValue) | ||||
|         { | ||||
|             // Arrange | ||||
|             var converter = new CommandOptionInputConverter(); | ||||
|  | ||||
|             // Act | ||||
|             var convertedValue = converter.ConvertOptionInput(optionInput, targetType); | ||||
|  | ||||
|             // Assert | ||||
|             convertedValue.Should().BeEquivalentTo(expectedConvertedValue); | ||||
|             convertedValue?.Should().BeAssignableTo(targetType); | ||||
|         } | ||||
|  | ||||
|         [Test] | ||||
|         [TestCaseSource(nameof(GetTestCases_ConvertOptionInput_Negative))] | ||||
|         public void ConvertOptionInput_Negative_Test(CommandOptionInput optionInput, Type targetType) | ||||
|         { | ||||
|             // Arrange | ||||
|             var converter = new CommandOptionInputConverter(); | ||||
|  | ||||
|             // Act & Assert | ||||
|             converter.Invoking(c => c.ConvertOptionInput(optionInput, targetType)) | ||||
|                 .Should().ThrowExactly<CliFxException>(); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -1,116 +0,0 @@ | ||||
| using FluentAssertions; | ||||
| using NUnit.Framework; | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
| using CliFx.Exceptions; | ||||
| using CliFx.Models; | ||||
| using CliFx.Services; | ||||
| using CliFx.Tests.TestCommands; | ||||
|  | ||||
| namespace CliFx.Tests.Services | ||||
| { | ||||
|     [TestFixture] | ||||
|     public class CommandSchemaResolverTests | ||||
|     { | ||||
|         private static IEnumerable<TestCaseData> GetTestCases_GetCommandSchemas() | ||||
|         { | ||||
|             yield return new TestCaseData( | ||||
|                 new[] { typeof(DivideCommand), typeof(ConcatCommand), typeof(EnvironmentVariableCommand) }, | ||||
|                 new[] | ||||
|                 { | ||||
|                     new CommandSchema(typeof(DivideCommand), "div", "Divide one number by another.", | ||||
|                         new[] | ||||
|                         { | ||||
|                             new CommandOptionSchema(typeof(DivideCommand).GetProperty(nameof(DivideCommand.Dividend)), | ||||
|                                 "dividend", 'D', true, "The number to divide.", null), | ||||
|                             new CommandOptionSchema(typeof(DivideCommand).GetProperty(nameof(DivideCommand.Divisor)), | ||||
|                                 "divisor", 'd', true, "The number to divide by.", null) | ||||
|                         }), | ||||
|                     new CommandSchema(typeof(ConcatCommand), "concat", "Concatenate strings.", | ||||
|                         new[] | ||||
|                         { | ||||
|                             new CommandOptionSchema(typeof(ConcatCommand).GetProperty(nameof(ConcatCommand.Inputs)), | ||||
|                                 null, 'i', true, "Input strings.", null), | ||||
|                             new CommandOptionSchema(typeof(ConcatCommand).GetProperty(nameof(ConcatCommand.Separator)), | ||||
|                                 null, 's', false, "String separator.", null) | ||||
|                         }), | ||||
|                     new CommandSchema(typeof(EnvironmentVariableCommand), null, "Reads option values from environment variables.", | ||||
|                         new[] | ||||
|                         { | ||||
|                             new CommandOptionSchema(typeof(EnvironmentVariableCommand).GetProperty(nameof(EnvironmentVariableCommand.Option)), | ||||
|                                 "opt", null, false, null, "ENV_SINGLE_VALUE") | ||||
|                         } | ||||
|                     ) | ||||
|                 } | ||||
|             ); | ||||
|  | ||||
|             yield return new TestCaseData( | ||||
|                 new[] { typeof(HelloWorldDefaultCommand) }, | ||||
|                 new[] | ||||
|                 { | ||||
|                     new CommandSchema(typeof(HelloWorldDefaultCommand), null, null, new CommandOptionSchema[0]) | ||||
|                 } | ||||
|             ); | ||||
|         } | ||||
|  | ||||
|         private static IEnumerable<TestCaseData> GetTestCases_GetCommandSchemas_Negative() | ||||
|         { | ||||
|             yield return new TestCaseData(new object[] | ||||
|             { | ||||
|                 new Type[0] | ||||
|             }); | ||||
|  | ||||
|             yield return new TestCaseData(new object[] | ||||
|             { | ||||
|                 new[] {typeof(NonImplementedCommand)} | ||||
|             }); | ||||
|  | ||||
|             yield return new TestCaseData(new object[] | ||||
|             { | ||||
|                 new[] {typeof(NonAnnotatedCommand)} | ||||
|             }); | ||||
|  | ||||
|             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(ExceptionCommand), typeof(CommandExceptionCommand)} | ||||
|             }); | ||||
|         } | ||||
|  | ||||
|         [Test] | ||||
|         [TestCaseSource(nameof(GetTestCases_GetCommandSchemas))] | ||||
|         public void GetCommandSchemas_Test(IReadOnlyList<Type> commandTypes, | ||||
|             IReadOnlyList<CommandSchema> expectedCommandSchemas) | ||||
|         { | ||||
|             // Arrange | ||||
|             var commandSchemaResolver = new CommandSchemaResolver(); | ||||
|  | ||||
|             // Act | ||||
|             var commandSchemas = commandSchemaResolver.GetCommandSchemas(commandTypes); | ||||
|  | ||||
|             // Assert | ||||
|             commandSchemas.Should().BeEquivalentTo(expectedCommandSchemas); | ||||
|         } | ||||
|  | ||||
|         [Test] | ||||
|         [TestCaseSource(nameof(GetTestCases_GetCommandSchemas_Negative))] | ||||
|         public void GetCommandSchemas_Negative_Test(IReadOnlyList<Type> commandTypes) | ||||
|         { | ||||
|             // Arrange | ||||
|             var resolver = new CommandSchemaResolver(); | ||||
|  | ||||
|             // Act & Assert | ||||
|             resolver.Invoking(r => r.GetCommandSchemas(commandTypes)) | ||||
|                 .Should().ThrowExactly<CliFxException>(); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -1,40 +0,0 @@ | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
| using System.Linq; | ||||
| using CliFx.Models; | ||||
| using CliFx.Services; | ||||
| using CliFx.Tests.TestCommands; | ||||
| using FluentAssertions; | ||||
| using NUnit.Framework; | ||||
|  | ||||
| namespace CliFx.Tests.Services | ||||
| { | ||||
|     [TestFixture] | ||||
|     public class DelegateCommandFactoryTests | ||||
|     { | ||||
|         private static CommandSchema GetCommandSchema(Type commandType) => | ||||
|             new CommandSchemaResolver().GetCommandSchemas(new[] {commandType}).Single(); | ||||
|  | ||||
|         private static IEnumerable<TestCaseData> GetTestCases_CreateCommand() | ||||
|         { | ||||
|             yield return new TestCaseData( | ||||
|                 new Func<CommandSchema, ICommand>(schema => (ICommand) Activator.CreateInstance(schema.Type)), | ||||
|                 GetCommandSchema(typeof(HelloWorldDefaultCommand)) | ||||
|             ); | ||||
|         } | ||||
|  | ||||
|         [Test] | ||||
|         [TestCaseSource(nameof(GetTestCases_CreateCommand))] | ||||
|         public void CreateCommand_Test(Func<CommandSchema, ICommand> factoryMethod, CommandSchema commandSchema) | ||||
|         { | ||||
|             // Arrange | ||||
|             var factory = new DelegateCommandFactory(factoryMethod); | ||||
|  | ||||
|             // Act | ||||
|             var command = factory.CreateCommand(commandSchema); | ||||
|  | ||||
|             // Assert | ||||
|             command.Should().BeOfType(commandSchema.Type); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -1,109 +0,0 @@ | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
| using System.IO; | ||||
| using System.Linq; | ||||
| using CliFx.Models; | ||||
| using CliFx.Services; | ||||
| using CliFx.Tests.TestCommands; | ||||
| using FluentAssertions; | ||||
| using NUnit.Framework; | ||||
|  | ||||
| namespace CliFx.Tests.Services | ||||
| { | ||||
|     [TestFixture] | ||||
|     public class HelpTextRendererTests | ||||
|     { | ||||
|         private static HelpTextSource CreateHelpTextSource(IReadOnlyList<Type> availableCommandTypes, Type targetCommandType) | ||||
|         { | ||||
|             var commandSchemaResolver = new CommandSchemaResolver(); | ||||
|  | ||||
|             var applicationMetadata = new ApplicationMetadata("TestApp", "testapp", "1.0", null); | ||||
|             var availableCommandSchemas = commandSchemaResolver.GetCommandSchemas(availableCommandTypes); | ||||
|             var targetCommandSchema = availableCommandSchemas.Single(s => s.Type == targetCommandType); | ||||
|  | ||||
|             return new HelpTextSource(applicationMetadata, availableCommandSchemas, targetCommandSchema); | ||||
|         } | ||||
|  | ||||
|         private static IEnumerable<TestCaseData> GetTestCases_RenderHelpText() | ||||
|         { | ||||
|             yield return new TestCaseData( | ||||
|                 CreateHelpTextSource( | ||||
|                     new[] {typeof(HelpDefaultCommand), typeof(HelpNamedCommand), typeof(HelpSubCommand)}, | ||||
|                     typeof(HelpDefaultCommand)), | ||||
|  | ||||
|                 new[] | ||||
|                 { | ||||
|                     "Description", | ||||
|                     "HelpDefaultCommand description.", | ||||
|                     "Usage", | ||||
|                     "[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( | ||||
|                 CreateHelpTextSource( | ||||
|                     new[] {typeof(HelpDefaultCommand), typeof(HelpNamedCommand), typeof(HelpSubCommand)}, | ||||
|                     typeof(HelpNamedCommand)), | ||||
|  | ||||
|                 new[] | ||||
|                 { | ||||
|                     "Description", | ||||
|                     "HelpNamedCommand description.", | ||||
|                     "Usage", | ||||
|                     "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( | ||||
|                 CreateHelpTextSource( | ||||
|                     new[] {typeof(HelpDefaultCommand), typeof(HelpNamedCommand), typeof(HelpSubCommand)}, | ||||
|                     typeof(HelpSubCommand)), | ||||
|  | ||||
|                 new[] | ||||
|                 { | ||||
|                     "Description", | ||||
|                     "HelpSubCommand description.", | ||||
|                     "Usage", | ||||
|                     "cmd sub", "[options]", | ||||
|                     "Options", | ||||
|                     "-e|--option-e", "OptionE description.", | ||||
|                     "-h|--help", "Shows help text." | ||||
|                 } | ||||
|             ); | ||||
|         } | ||||
|  | ||||
|         [Test] | ||||
|         [TestCaseSource(nameof(GetTestCases_RenderHelpText))] | ||||
|         public void RenderHelpText_Test(HelpTextSource source, | ||||
|             IReadOnlyList<string> expectedSubstrings) | ||||
|         { | ||||
|             // Arrange | ||||
|             using (var stdout = new StringWriter()) | ||||
|             { | ||||
|                 var console = new VirtualConsole(stdout); | ||||
|                 var renderer = new HelpTextRenderer(); | ||||
|  | ||||
|                 // Act | ||||
|                 renderer.RenderHelpText(console, source); | ||||
|  | ||||
|                 // Assert | ||||
|                 stdout.ToString().Should().ContainAll(expectedSubstrings); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -1,41 +0,0 @@ | ||||
| using System; | ||||
| using CliFx.Services; | ||||
| using FluentAssertions; | ||||
| using NUnit.Framework; | ||||
|  | ||||
| namespace CliFx.Tests.Services | ||||
| { | ||||
|     [TestFixture] | ||||
|     public class SystemConsoleTests | ||||
|     { | ||||
|         [TearDown] | ||||
|         public void TearDown() | ||||
|         { | ||||
|             // Reset console color so it doesn't carry on into next tests | ||||
|             Console.ResetColor(); | ||||
|         } | ||||
|          | ||||
|         // Make sure console correctly wraps around System.Console | ||||
|         [Test] | ||||
|         public void All_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,43 +0,0 @@ | ||||
| using System; | ||||
| using System.IO; | ||||
| using CliFx.Services; | ||||
| using FluentAssertions; | ||||
| using NUnit.Framework; | ||||
|  | ||||
| namespace CliFx.Tests.Services | ||||
| { | ||||
|     [TestFixture] | ||||
|     public class VirtualConsoleTests | ||||
|     { | ||||
|         // Make sure console uses specified streams and doesn't leak to System.Console | ||||
|         [Test] | ||||
|         public void All_Smoke_Test() | ||||
|         { | ||||
|             // Arrange | ||||
|             using (var stdin = new StringReader("hello world")) | ||||
|             using (var stdout = new StringWriter()) | ||||
|             using (var stderr = new StringWriter()) | ||||
|             { | ||||
|                 var console = new VirtualConsole(stdin, stdout, stderr); | ||||
|  | ||||
|                 // Act | ||||
|                 console.ResetColor(); | ||||
|                 console.ForegroundColor = ConsoleColor.DarkMagenta; | ||||
|                 console.BackgroundColor = ConsoleColor.DarkMagenta; | ||||
|  | ||||
|                 // Assert | ||||
|                 console.Input.Should().BeSameAs(stdin); | ||||
|                 console.Input.Should().NotBeSameAs(Console.In); | ||||
|                 console.IsInputRedirected.Should().BeTrue(); | ||||
|                 console.Output.Should().BeSameAs(stdout); | ||||
|                 console.Output.Should().NotBeSameAs(Console.Out); | ||||
|                 console.IsOutputRedirected.Should().BeTrue(); | ||||
|                 console.Error.Should().BeSameAs(stderr); | ||||
|                 console.Error.Should().NotBeSameAs(Console.Error); | ||||
|                 console.IsErrorRedirected.Should().BeTrue(); | ||||
|                 console.ForegroundColor.Should().NotBe(Console.ForegroundColor); | ||||
|                 console.BackgroundColor.Should().NotBe(Console.BackgroundColor); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										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,10 +0,0 @@ | ||||
| using System.Collections.Generic; | ||||
| using CliFx.Services; | ||||
|  | ||||
| namespace CliFx.Tests.Stubs | ||||
| { | ||||
|     public class EmptyEnvironmentVariablesProviderStub : IEnvironmentVariablesProvider | ||||
|     { | ||||
|         public IReadOnlyDictionary<string, string> GetEnvironmentVariables() => new Dictionary<string, string>(); | ||||
|     } | ||||
| } | ||||
| @@ -1,18 +0,0 @@ | ||||
| using System.Collections.Generic; | ||||
| using System.IO; | ||||
| using CliFx.Services; | ||||
|  | ||||
| namespace CliFx.Tests.Stubs | ||||
| { | ||||
|     public class EnvironmentVariablesProviderStub : IEnvironmentVariablesProvider | ||||
|     { | ||||
|         public static readonly Dictionary<string, string> EnvironmentVariables = new Dictionary<string, string> | ||||
|         { | ||||
|             ["ENV_SINGLE_VALUE"] = "A", | ||||
|             ["ENV_MULTIPLE_VALUES"] = $"A{Path.PathSeparator}B{Path.PathSeparator}C{Path.PathSeparator}", | ||||
|             ["ENV_ESCAPED_MULTIPLE_VALUES"] = $"\"A{Path.PathSeparator}B{Path.PathSeparator}C{Path.PathSeparator}\"" | ||||
|         }; | ||||
|  | ||||
|         public IReadOnlyDictionary<string, string> GetEnvironmentVariables() => EnvironmentVariables; | ||||
|     } | ||||
| } | ||||
| @@ -1,23 +0,0 @@ | ||||
| using System; | ||||
| using System.Threading; | ||||
| using System.Threading.Tasks; | ||||
| using CliFx.Attributes; | ||||
| using CliFx.Services; | ||||
|  | ||||
| namespace CliFx.Tests.TestCommands | ||||
| { | ||||
|     [Command("cancel")] | ||||
|     public class CancellableCommand : ICommand | ||||
|     { | ||||
|         public async Task ExecuteAsync(IConsole console, CancellationToken cancellationToken) | ||||
|         { | ||||
|             await Task.Yield(); | ||||
|  | ||||
|             console.Output.WriteLine("Printed"); | ||||
|  | ||||
|             await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken).ConfigureAwait(false); | ||||
|  | ||||
|             console.Output.WriteLine("Never printed"); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -1,20 +0,0 @@ | ||||
| using System.Threading; | ||||
| using System.Threading.Tasks; | ||||
| using CliFx.Attributes; | ||||
| using CliFx.Exceptions; | ||||
| using CliFx.Services; | ||||
|  | ||||
| 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 Task ExecuteAsync(IConsole console, CancellationToken cancellationToken) => throw new CommandException(Message, ExitCode); | ||||
|     } | ||||
| } | ||||
| @@ -1,24 +0,0 @@ | ||||
| using System.Collections.Generic; | ||||
| using System.Threading; | ||||
| using System.Threading.Tasks; | ||||
| using CliFx.Attributes; | ||||
| using CliFx.Services; | ||||
|  | ||||
| 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 Task ExecuteAsync(IConsole console, CancellationToken cancellationToken) | ||||
|         { | ||||
|             console.Output.WriteLine(string.Join(Separator, Inputs)); | ||||
|             return Task.CompletedTask; | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -1,26 +0,0 @@ | ||||
| using System.Threading; | ||||
| using System.Threading.Tasks; | ||||
| using CliFx.Attributes; | ||||
| using CliFx.Services; | ||||
|  | ||||
| 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 Task ExecuteAsync(IConsole console, CancellationToken cancellationToken) | ||||
|         { | ||||
|             console.Output.WriteLine(Dividend / Divisor); | ||||
|             return Task.CompletedTask; | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -1,19 +0,0 @@ | ||||
| using System.Threading; | ||||
| using System.Threading.Tasks; | ||||
| using CliFx.Attributes; | ||||
| using CliFx.Services; | ||||
|  | ||||
| namespace CliFx.Tests.TestCommands | ||||
| { | ||||
|     [Command] | ||||
|     public class DuplicateOptionNamesCommand : ICommand | ||||
|     { | ||||
|         [CommandOption("fruits")] | ||||
|         public string Apples { get; set; } | ||||
|          | ||||
|         [CommandOption("fruits")] | ||||
|         public string Oranges { get; set; } | ||||
|          | ||||
|         public Task ExecuteAsync(IConsole console, CancellationToken cancellationToken) => Task.CompletedTask; | ||||
|     } | ||||
| } | ||||
| @@ -1,19 +0,0 @@ | ||||
| using System.Threading; | ||||
| using System.Threading.Tasks; | ||||
| using CliFx.Attributes; | ||||
| using CliFx.Services; | ||||
|  | ||||
| namespace CliFx.Tests.TestCommands | ||||
| { | ||||
|     [Command] | ||||
|     public class DuplicateOptionShortNamesCommand : ICommand | ||||
|     { | ||||
|         [CommandOption('f')] | ||||
|         public string Apples { get; set; } | ||||
|          | ||||
|         [CommandOption('f')] | ||||
|         public string Oranges { get; set; } | ||||
|          | ||||
|         public Task ExecuteAsync(IConsole console, CancellationToken cancellationToken) => Task.CompletedTask; | ||||
|     } | ||||
| } | ||||
| @@ -1,16 +0,0 @@ | ||||
| using System.Threading; | ||||
| using System.Threading.Tasks; | ||||
| using CliFx.Attributes; | ||||
| using CliFx.Services; | ||||
|  | ||||
| namespace CliFx.Tests.TestCommands | ||||
| { | ||||
| 	[Command(Description = "Reads option values from environment variables.")] | ||||
| 	public class EnvironmentVariableCommand : ICommand | ||||
| 	{ | ||||
| 		[CommandOption("opt", EnvironmentVariableName = "ENV_SINGLE_VALUE")] | ||||
| 		public string Option { get; set; } | ||||
|  | ||||
| 		public Task ExecuteAsync(IConsole console, CancellationToken cancellationToken) => Task.CompletedTask; | ||||
| 	} | ||||
| } | ||||
| @@ -1,17 +0,0 @@ | ||||
| using System.Collections.Generic; | ||||
| using System.Threading; | ||||
| using System.Threading.Tasks; | ||||
| using CliFx.Attributes; | ||||
| using CliFx.Services; | ||||
|  | ||||
| namespace CliFx.Tests.TestCommands | ||||
| { | ||||
| 	[Command(Description = "Reads multiple option values from environment variables.")] | ||||
| 	public class EnvironmentVariableWithMultipleValuesCommand : ICommand | ||||
| 	{ | ||||
| 		[CommandOption("opt", EnvironmentVariableName = "ENV_MULTIPLE_VALUES")] | ||||
| 		public IEnumerable<string> Option { get; set; } | ||||
|  | ||||
| 		public Task ExecuteAsync(IConsole console, CancellationToken cancellationToken) => Task.CompletedTask; | ||||
| 	} | ||||
| } | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user