├── .config └── dotnet-tools.json ├── .editorconfig ├── .gitattributes ├── .github ├── CODEOWNERS ├── actions │ ├── install-tools │ │ └── action.yml │ └── version-vars │ │ └── action.yml ├── dependabot.yml └── workflows │ ├── CI.yml │ ├── PR.yml │ ├── workflow_build.yml │ └── workflow_release.yml ├── .gitignore ├── .vscode ├── extensions.json └── settings.json ├── Directory.Build.props ├── Directory.Build.rsp ├── Directory.Packages.props ├── Docs ├── BuildingAndTesting.md ├── README.md └── Research.md ├── LICENSE.md ├── README.md ├── codecov.yml ├── eng ├── Defaults.props ├── DotNetAnalyzers.props ├── DotNetDefaults.props └── Packaging.props ├── global.json ├── nuget.config ├── scripts ├── PatchGlobalJson.ps1 └── UpdateVSVersions.ps1 ├── slnup.slnx ├── src ├── .editorconfig ├── Directory.Build.props ├── SlnUp.Core │ ├── ConsoleHelpers.cs │ ├── Extensions │ │ ├── RegexExtensions.cs │ │ └── VersionExtensions.cs │ ├── ScopedAction.cs │ ├── SlnUp.Core.csproj │ ├── VersionManager.cs │ ├── VersionManager.g.cs │ ├── VisualStudioProduct.cs │ └── VisualStudioVersion.cs ├── SlnUp.Json │ ├── Extensions │ │ └── AssemblyExtensions.cs │ ├── SlnUp.Json.csproj │ ├── VersionManagerJsonHelper.cs │ └── VisualStudioVersionJsonHelper.cs ├── SlnUp │ ├── CLI │ │ ├── ArgumentParser.cs │ │ └── ProgramOptions.cs │ ├── Program.cs │ ├── SlnUp.csproj │ ├── SlnUpOptions.cs │ ├── SolutionFile.cs │ └── SolutionFileHeader.cs └── VisualStudio.VersionScraper │ ├── OutputFormat.cs │ ├── Program.cs │ ├── ProgramOptions.cs │ ├── ProgramOptionsBinder.cs │ ├── Properties │ └── launchSettings.json │ ├── VisualStudio.VersionScraper.csproj │ ├── VisualStudioVersionDocScraper.cs │ └── Writers │ ├── CSharp │ └── CSharpVersionWriter.cs │ └── CodeWriter.cs ├── tests ├── .editorconfig ├── Directory.Build.props ├── SlnUp.Core.Tests │ ├── Extensions │ │ └── VersionExtensionsTests.cs │ ├── ScopedActionTests.cs │ ├── SlnUp.Core.Tests.csproj │ ├── TestVersions.json │ ├── VersionManagerTests.cs │ └── VisualStudioVersionTests.cs ├── SlnUp.Json.Tests │ ├── Extensions │ │ └── AssemblyExtensionsTests.cs │ ├── SlnUp.Json.Tests.csproj │ ├── VersionManagerJsonHelperTests.cs │ └── VisualStudioVersionJsonHelperTests.cs ├── SlnUp.TestLibrary │ ├── Extensions │ │ └── StringExtensions.cs │ ├── ScopedDirectory.cs │ ├── ScopedFile.cs │ ├── SlnUp.TestLibrary.csproj │ ├── TemporaryDirectory.cs │ ├── TemporaryFile.cs │ └── WorkingDirectory.cs └── SlnUp.Tests │ ├── CLI │ ├── ArgumentParserTests.cs │ └── ProgramOptionsTests.cs │ ├── ProgramTests.cs │ ├── SlnUp.Tests.csproj │ ├── SolutionFileBuilder.cs │ ├── SolutionFileBuilderTests.cs │ ├── SolutionFileHeaderTests.cs │ ├── SolutionFileTests.cs │ └── Utilities │ └── TestConsoleExtensions.cs └── version.json /.config/dotnet-tools.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "isRoot": true, 4 | "tools": { 5 | "nbgv": { 6 | "version": "3.7.115", 7 | "commands": [ 8 | "nbgv" 9 | ] 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: https://EditorConfig.org 2 | # You can learn more about editorconfig here: https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-code-style-settings-reference 3 | 4 | # IMPORTANT 5 | # This file is intended to be a static template. It shouldn't be modified for project specific settings. 6 | # Place project specific settings in subfolders whenever possible. 7 | 8 | # top-most EditorConfig file 9 | root = true 10 | 11 | [*] 12 | charset = utf-8-bom 13 | indent_style = space 14 | insert_final_newline = false 15 | trim_trailing_whitespace = true 16 | 17 | # Add guidelines to editor: https://github.com/pharring/EditorGuidelines#editorconfig-support-vs-2017-or-above 18 | guidelines = 80, 120 19 | 20 | [*.{yml,yaml}] 21 | indent_size = 2 22 | insert_final_newline = true 23 | 24 | [*.{props,csproj,sln,targets}] 25 | indent_size = 2 26 | insert_final_newline = true 27 | 28 | [*.md] 29 | insert_final_newline = true 30 | 31 | [*.{cs,vb}] 32 | 33 | indent_size = 4 34 | tab_width = 4 35 | insert_final_newline = true 36 | 37 | # .NET Analyzer severities 38 | 39 | # Default severity for all analyzer diagnostics 40 | dotnet_analyzer_diagnostic.severity = warning 41 | 42 | # CA1014: Mark assemblies with CLSCompliantAttribute 43 | dotnet_diagnostic.CA1014.severity = none 44 | 45 | # CA1031: Do not catch general exception types 46 | dotnet_diagnostic.CA1031.severity = suggestion 47 | 48 | # IDE0008: Use explicit type instead of var 49 | dotnet_diagnostic.IDE0008.severity = error 50 | 51 | # IDE0021: Use expression body for constructors 52 | dotnet_diagnostic.IDE0021.severity = suggestion 53 | 54 | # IDE0022: Use expression body for methods 55 | dotnet_diagnostic.IDE0022.severity = suggestion 56 | 57 | # IDE0028: Use collection initializers or expressions 58 | dotnet_diagnostic.IDE0028.severity = suggestion 59 | 60 | # IDE0046: Use conditional expression for return 61 | dotnet_diagnostic.IDE0046.severity = suggestion 62 | 63 | # IDE0058: Remove unnecessary expression value 64 | dotnet_diagnostic.IDE0058.severity = suggestion 65 | 66 | # IDE0059: Remove unnecessary value assignment 67 | dotnet_diagnostic.IDE0059.severity = suggestion 68 | 69 | #### Naming styles #### 70 | 71 | # Naming rules 72 | 73 | dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface 74 | dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i 75 | 76 | dotnet_naming_rule.types_should_be_pascal_case.symbols = types 77 | dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case 78 | 79 | dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members 80 | dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case 81 | 82 | # Symbol specifications 83 | 84 | dotnet_naming_symbols.interface.applicable_kinds = interface 85 | dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected 86 | dotnet_naming_symbols.interface.required_modifiers = 87 | 88 | dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum 89 | dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected 90 | dotnet_naming_symbols.types.required_modifiers = 91 | 92 | dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method 93 | dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected 94 | dotnet_naming_symbols.non_field_members.required_modifiers = 95 | 96 | # Naming styles 97 | 98 | dotnet_naming_style.begins_with_i.required_prefix = I 99 | dotnet_naming_style.begins_with_i.required_suffix = 100 | dotnet_naming_style.begins_with_i.word_separator = 101 | dotnet_naming_style.begins_with_i.capitalization = pascal_case 102 | 103 | dotnet_naming_style.pascal_case.required_prefix = 104 | dotnet_naming_style.pascal_case.required_suffix = 105 | dotnet_naming_style.pascal_case.word_separator = 106 | dotnet_naming_style.pascal_case.capitalization = pascal_case 107 | 108 | dotnet_naming_style.pascal_case.required_prefix = 109 | dotnet_naming_style.pascal_case.required_suffix = 110 | dotnet_naming_style.pascal_case.word_separator = 111 | dotnet_naming_style.pascal_case.capitalization = pascal_case 112 | 113 | #Style - language keyword and framework type options 114 | 115 | #prefer the language keyword for local variables, method parameters, and class members, instead of the type name, for types that have a keyword to represent them 116 | dotnet_style_predefined_type_for_locals_parameters_members = true 117 | 118 | #Style - qualification options 119 | 120 | #prefer fields to be prefaced with this. in C# or Me. in Visual Basic 121 | dotnet_style_qualification_for_field = true 122 | #prefer methods to be prefaced with this. in C# or Me. in Visual Basic 123 | dotnet_style_qualification_for_method = true 124 | #prefer properties to be prefaced with this. in C# or Me. in Visual Basic 125 | dotnet_style_qualification_for_property = true 126 | 127 | dotnet_style_operator_placement_when_wrapping = beginning_of_line 128 | dotnet_style_coalesce_expression = true 129 | dotnet_style_null_propagation = true 130 | dotnet_style_prefer_is_null_check_over_reference_equality_method = true 131 | dotnet_style_prefer_auto_properties = true 132 | dotnet_style_object_initializer = true 133 | dotnet_style_collection_initializer = true 134 | dotnet_style_prefer_simplified_boolean_expressions = true 135 | dotnet_style_prefer_conditional_expression_over_assignment = true 136 | dotnet_style_prefer_conditional_expression_over_return = true 137 | dotnet_style_explicit_tuple_names = true 138 | dotnet_style_prefer_inferred_tuple_names = true 139 | dotnet_style_prefer_inferred_anonymous_type_member_names = true 140 | dotnet_style_prefer_compound_assignment = true 141 | dotnet_style_prefer_simplified_interpolation = true 142 | dotnet_style_namespace_match_folder = true 143 | dotnet_style_readonly_field = true 144 | 145 | #Formatting - organize using options 146 | # https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/dotnet-formatting-options 147 | dotnet_sort_system_directives_first = true 148 | dotnet_separate_import_directive_groups = true 149 | 150 | [*.cs] 151 | 152 | #Formatting - spacing options 153 | 154 | #require a space before the colon for bases or interfaces in a type declaration 155 | csharp_space_after_colon_in_inheritance_clause = true 156 | #require a space after a keyword in a control flow statement such as a for loop 157 | csharp_space_after_keywords_in_control_flow_statements = true 158 | #require a space before the colon for bases or interfaces in a type declaration 159 | csharp_space_before_colon_in_inheritance_clause = true 160 | #remove space within empty argument list parentheses 161 | csharp_space_between_method_call_empty_parameter_list_parentheses = false 162 | #remove space between method call name and opening parenthesis 163 | csharp_space_between_method_call_name_and_opening_parenthesis = false 164 | #do not place space characters after the opening parenthesis and before the closing parenthesis of a method call 165 | csharp_space_between_method_call_parameter_list_parentheses = false 166 | #remove space within empty parameter list parentheses for a method declaration 167 | csharp_space_between_method_declaration_empty_parameter_list_parentheses = false 168 | #place a space character after the opening parenthesis and before the closing parenthesis of a method declaration parameter list. 169 | csharp_space_between_method_declaration_parameter_list_parentheses = false 170 | 171 | #Formatting - wrapping options 172 | 173 | #leave code block on single line 174 | csharp_preserve_single_line_blocks = true 175 | 176 | #Style - expression bodied member options 177 | 178 | #prefer block bodies for constructors 179 | csharp_style_expression_bodied_constructors = when_on_single_line 180 | 181 | #prefer expression-bodied members for properties 182 | csharp_style_expression_bodied_properties = when_on_single_line 183 | 184 | #Style - expression level options 185 | 186 | #prefer out variables to be declared before the method call 187 | csharp_style_inlined_variable_declaration = false 188 | 189 | #Style - implicit and explicit types 190 | 191 | 192 | # IDE0007 and IDE0008: 'var' preferences 193 | csharp_style_var_for_built_in_types = false 194 | csharp_style_var_when_type_is_apparent = false 195 | csharp_style_var_elsewhere = false 196 | 197 | # IDE0160 and IDE0161: Namespace declaration preferences 198 | csharp_style_namespace_declarations = file_scoped 199 | 200 | # IDE0022: Use block body for methods 201 | csharp_style_expression_bodied_methods = when_on_single_line 202 | 203 | # IDE0023: Use block body for operators 204 | csharp_style_expression_bodied_operators = when_on_single_line 205 | 206 | # IDE0090: Use 'new(...)' 207 | csharp_style_implicit_object_creation_when_type_is_apparent = true 208 | 209 | # IDE0290: Use Primary Constructors 210 | csharp_style_prefer_primary_constructors = false 211 | 212 | csharp_indent_labels = one_less_than_current 213 | csharp_space_around_binary_operators = before_and_after 214 | csharp_using_directive_placement = inside_namespace 215 | csharp_prefer_simple_using_statement = true 216 | csharp_prefer_braces = true 217 | csharp_style_prefer_method_group_conversion = true 218 | csharp_style_prefer_top_level_statements = false 219 | csharp_style_expression_bodied_indexers = true 220 | csharp_style_expression_bodied_accessors = true 221 | csharp_style_expression_bodied_lambdas = true 222 | csharp_style_expression_bodied_local_functions = false 223 | csharp_style_throw_expression = true 224 | csharp_style_prefer_null_check_over_type_check = true 225 | csharp_prefer_simple_default_expression = true 226 | csharp_style_prefer_local_over_anonymous_function = true 227 | csharp_style_prefer_index_operator = true 228 | csharp_style_prefer_range_operator = true 229 | csharp_style_prefer_tuple_swap = true 230 | csharp_style_prefer_utf8_string_literals = true 231 | csharp_style_deconstructed_variable_declaration = true 232 | 233 | # IDE0058: Remove unnecessary expression value 234 | csharp_style_unused_value_expression_statement_preference = discard_variable 235 | 236 | # IDE0059: Remove unnecessary value assignment 237 | csharp_style_unused_value_assignment_preference = discard_variable 238 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # https://docs.github.com/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners 2 | * @craigktreasure 3 | -------------------------------------------------------------------------------- /.github/actions/install-tools/action.yml: -------------------------------------------------------------------------------- 1 | name: Install tools 2 | 3 | runs: 4 | using: "composite" 5 | steps: 6 | - name: Setup .NET SDK 7 | uses: actions/setup-dotnet@v4 8 | with: 9 | global-json-file: global.json 10 | 11 | - name: Setup additional .NET SDK versions 12 | uses: actions/setup-dotnet@v4 13 | with: 14 | dotnet-version: | 15 | 8.x 16 | 17 | - name: Install .NET tools 18 | shell: pwsh 19 | run: dotnet tool restore 20 | -------------------------------------------------------------------------------- /.github/actions/version-vars/action.yml: -------------------------------------------------------------------------------- 1 | name: Set version variables 2 | 3 | outputs: 4 | package_version: 5 | description: The package version. 6 | value: ${{ steps.version.outputs.package_version }} 7 | 8 | runs: 9 | using: "composite" 10 | steps: 11 | - name: Set version 12 | id: version 13 | shell: pwsh 14 | run: | 15 | $packageVersion = dotnet nbgv get-version --variable NuGetPackageVersion 16 | "package_version=$packageVersion" >> $env:GITHUB_OUTPUT 17 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: nuget 4 | directory: "/" 5 | schedule: 6 | interval: weekly 7 | day: 'friday' 8 | time: '17:00' 9 | timezone: 'America/Los_Angeles' 10 | open-pull-requests-limit: 10 11 | reviewers: 12 | - craigktreasure 13 | assignees: 14 | - craigktreasure 15 | groups: 16 | xunit: 17 | patterns: 18 | - "xunit*" 19 | gitversioning: 20 | patterns: 21 | - "Nerdbank.GitVersioning" 22 | - "nbgv" 23 | 24 | - package-ecosystem: 'dotnet-sdk' 25 | directory: "/" 26 | schedule: 27 | interval: 'weekly' 28 | day: 'tuesday' 29 | time: '22:00' 30 | 31 | - package-ecosystem: github-actions 32 | directory: "/" 33 | schedule: 34 | interval: weekly 35 | day: 'friday' 36 | time: '17:00' 37 | timezone: 'America/Los_Angeles' 38 | open-pull-requests-limit: 10 39 | reviewers: 40 | - craigktreasure 41 | assignees: 42 | - craigktreasure 43 | -------------------------------------------------------------------------------- /.github/workflows/CI.yml: -------------------------------------------------------------------------------- 1 | name: SlnUp-CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | paths-ignore: 8 | - '.config/**' 9 | - '.github/dependabot.yml' 10 | - '.vscode/**' 11 | - 'docs/**' 12 | - 'README.md' 13 | 14 | jobs: 15 | build_ci: 16 | name: Build SlnUp 17 | if: "!contains(github.event.head_commit.message, 'ci skip')" 18 | uses: ./.github/workflows/workflow_build.yml 19 | secrets: inherit 20 | with: 21 | # don't check format on CI builds due to common breaking changes in the .NET SDK 22 | checkFormat: false 23 | 24 | release_ci: 25 | name: Release SlnUp to NuGet.org 26 | needs: build_ci 27 | if: ${{ !contains(needs.build_ci.outputs.package_version, '-') }} 28 | uses: ./.github/workflows/workflow_release.yml 29 | secrets: inherit 30 | with: 31 | package-version: ${{ needs.build_ci.outputs.package_version }} 32 | -------------------------------------------------------------------------------- /.github/workflows/PR.yml: -------------------------------------------------------------------------------- 1 | name: SlnUp-PR 2 | 3 | on: pull_request 4 | 5 | jobs: 6 | build_pr: 7 | name: Build SlnUp 8 | strategy: 9 | max-parallel: 3 10 | fail-fast: false 11 | matrix: 12 | platform: [ 13 | { os: windows, buildAgent: windows-2025 }, 14 | { os: ubuntu, buildAgent: ubuntu-latest }, 15 | { os: macos, buildAgent: macos-14 } 16 | ] 17 | uses: ./.github/workflows/workflow_build.yml 18 | secrets: inherit 19 | with: 20 | artifactSuffix: ${{ matrix.platform.os }} 21 | buildAgent: ${{ matrix.platform.buildAgent }} 22 | -------------------------------------------------------------------------------- /.github/workflows/workflow_build.yml: -------------------------------------------------------------------------------- 1 | name: Build Workflow 2 | 3 | on: 4 | workflow_call: 5 | inputs: 6 | checkFormat: 7 | type: boolean 8 | default: true 9 | artifactSuffix: 10 | type: string 11 | default: ubuntu 12 | buildAgent: 13 | type: string 14 | default: ubuntu-latest 15 | outputs: 16 | package_version: 17 | description: 'The version of the package that was built.' 18 | value: ${{ jobs.build.outputs.package_version }} 19 | 20 | env: 21 | # Stop wasting time caching packages 22 | DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true 23 | 24 | jobs: 25 | build: 26 | name: Build 27 | runs-on: ${{ inputs.buildAgent }} 28 | 29 | outputs: 30 | package_version: ${{steps.version.outputs.package_version}} 31 | 32 | steps: 33 | - uses: actions/checkout@v4 34 | with: 35 | fetch-depth: 0 36 | 37 | - name: Install tools 38 | uses: ./.github/actions/install-tools 39 | 40 | - name: Set version variables 41 | id: version 42 | uses: ./.github/actions/version-vars 43 | 44 | - name: Restore 45 | run: dotnet restore 46 | 47 | - name: Format validation 48 | if: ${{ inputs.checkFormat }} 49 | run: dotnet format --no-restore --verify-no-changes 50 | 51 | - name: Build 52 | run: dotnet build --configuration Release --no-restore 53 | 54 | - name: Test 55 | run: dotnet test --configuration Release --no-build --verbosity normal /p:CollectCoverage=true 56 | 57 | - name: Upload coverage to Codecov 58 | uses: codecov/codecov-action@v5 59 | with: 60 | name: codecov 61 | directory: __artifacts/test-results 62 | token: ${{ secrets.CODECOV_TOKEN }} 63 | fail_ci_if_error: true 64 | verbose: true 65 | 66 | - name: Upload output artifact 67 | uses: actions/upload-artifact@v4 68 | with: 69 | name: output_${{ inputs.artifactSuffix }} 70 | path: __artifacts/bin 71 | 72 | - name: Upload package artifact 73 | uses: actions/upload-artifact@v4 74 | with: 75 | name: packages_${{ inputs.artifactSuffix }} 76 | path: __artifacts/package 77 | -------------------------------------------------------------------------------- /.github/workflows/workflow_release.yml: -------------------------------------------------------------------------------- 1 | name: Release Workflow 2 | 3 | on: 4 | workflow_call: 5 | inputs: 6 | package-version: 7 | type: string 8 | required: true 9 | 10 | jobs: 11 | release: 12 | name: Release 13 | runs-on: ubuntu-latest 14 | environment: 15 | name: NuGet.org 16 | url: https://www.nuget.org/packages/SlnUp/ 17 | 18 | steps: 19 | - name: Setup NuGet 20 | uses: NuGet/setup-nuget@v2 21 | with: 22 | nuget-version: latest 23 | 24 | - name: Download packages artifact 25 | uses: actions/download-artifact@v4 26 | with: 27 | name: packages_ubuntu 28 | path: __artifacts/package 29 | 30 | - name: Push ${{ inputs.package-version }} to NuGet.org 31 | run: nuget push __artifacts/package/release/SlnUp.*.nupkg -ApiKey ${{ secrets.NUGET_API_KEY }} -Source https://api.nuget.org/v3/index.json 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __* 2 | 3 | # Created by https://www.toptal.com/developers/gitignore/api/macos,windows,visualstudio,visualstudiocode 4 | # Edit at https://www.toptal.com/developers/gitignore?templates=macos,windows,visualstudio,visualstudiocode 5 | 6 | ### macOS ### 7 | # General 8 | .DS_Store 9 | .AppleDouble 10 | .LSOverride 11 | 12 | # Icon must end with two \r 13 | Icon 14 | 15 | 16 | # Thumbnails 17 | ._* 18 | 19 | # Files that might appear in the root of a volume 20 | .DocumentRevisions-V100 21 | .fseventsd 22 | .Spotlight-V100 23 | .TemporaryItems 24 | .Trashes 25 | .VolumeIcon.icns 26 | .com.apple.timemachine.donotpresent 27 | 28 | # Directories potentially created on remote AFP share 29 | .AppleDB 30 | .AppleDesktop 31 | Network Trash Folder 32 | Temporary Items 33 | .apdisk 34 | 35 | ### VisualStudioCode ### 36 | .vscode/* 37 | !.vscode/settings.json 38 | !.vscode/tasks.json 39 | !.vscode/launch.json 40 | !.vscode/extensions.json 41 | *.code-workspace 42 | 43 | # Local History for Visual Studio Code 44 | .history/ 45 | 46 | ### VisualStudioCode Patch ### 47 | # Ignore all local history of files 48 | .history 49 | .ionide 50 | 51 | # Support for Project snippet scope 52 | !.vscode/*.code-snippets 53 | 54 | ### Windows ### 55 | # Windows thumbnail cache files 56 | Thumbs.db 57 | Thumbs.db:encryptable 58 | ehthumbs.db 59 | ehthumbs_vista.db 60 | 61 | # Dump file 62 | *.stackdump 63 | 64 | # Folder config file 65 | [Dd]esktop.ini 66 | 67 | # Recycle Bin used on file shares 68 | $RECYCLE.BIN/ 69 | 70 | # Windows Installer files 71 | *.cab 72 | *.msi 73 | *.msix 74 | *.msm 75 | *.msp 76 | 77 | # Windows shortcuts 78 | *.lnk 79 | 80 | ### VisualStudio ### 81 | ## Ignore Visual Studio temporary files, build results, and 82 | ## files generated by popular Visual Studio add-ons. 83 | ## 84 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 85 | 86 | # User-specific files 87 | *.rsuser 88 | *.suo 89 | *.user 90 | *.userosscache 91 | *.sln.docstates 92 | 93 | # User-specific files (MonoDevelop/Xamarin Studio) 94 | *.userprefs 95 | 96 | # Mono auto generated files 97 | mono_crash.* 98 | 99 | # Build results 100 | [Dd]ebug/ 101 | [Dd]ebugPublic/ 102 | [Rr]elease/ 103 | [Rr]eleases/ 104 | x64/ 105 | x86/ 106 | [Ww][Ii][Nn]32/ 107 | [Aa][Rr][Mm]/ 108 | [Aa][Rr][Mm]64/ 109 | bld/ 110 | [Bb]in/ 111 | [Oo]bj/ 112 | [Ll]og/ 113 | [Ll]ogs/ 114 | 115 | # Visual Studio 2015/2017 cache/options directory 116 | .vs/ 117 | # Uncomment if you have tasks that create the project's static files in wwwroot 118 | #wwwroot/ 119 | 120 | # Visual Studio 2017 auto generated files 121 | Generated\ Files/ 122 | 123 | # MSTest test Results 124 | [Tt]est[Rr]esult*/ 125 | [Bb]uild[Ll]og.* 126 | 127 | # NUnit 128 | *.VisualState.xml 129 | TestResult.xml 130 | nunit-*.xml 131 | 132 | # Build Results of an ATL Project 133 | [Dd]ebugPS/ 134 | [Rr]eleasePS/ 135 | dlldata.c 136 | 137 | # Benchmark Results 138 | BenchmarkDotNet.Artifacts/ 139 | 140 | # .NET Core 141 | project.lock.json 142 | project.fragment.lock.json 143 | artifacts/ 144 | 145 | # ASP.NET Scaffolding 146 | ScaffoldingReadMe.txt 147 | 148 | # StyleCop 149 | StyleCopReport.xml 150 | 151 | # Files built by Visual Studio 152 | *_i.c 153 | *_p.c 154 | *_h.h 155 | *.ilk 156 | *.meta 157 | *.obj 158 | *.iobj 159 | *.pch 160 | *.pdb 161 | *.ipdb 162 | *.pgc 163 | *.pgd 164 | *.rsp 165 | !Directory.Build.rsp 166 | *.sbr 167 | *.tlb 168 | *.tli 169 | *.tlh 170 | *.tmp 171 | *.tmp_proj 172 | *_wpftmp.csproj 173 | *.log 174 | *.tlog 175 | *.vspscc 176 | *.vssscc 177 | .builds 178 | *.pidb 179 | *.svclog 180 | *.scc 181 | 182 | # Chutzpah Test files 183 | _Chutzpah* 184 | 185 | # Visual C++ cache files 186 | ipch/ 187 | *.aps 188 | *.ncb 189 | *.opendb 190 | *.opensdf 191 | *.sdf 192 | *.cachefile 193 | *.VC.db 194 | *.VC.VC.opendb 195 | 196 | # Visual Studio profiler 197 | *.psess 198 | *.vsp 199 | *.vspx 200 | *.sap 201 | 202 | # Visual Studio Trace Files 203 | *.e2e 204 | 205 | # TFS 2012 Local Workspace 206 | $tf/ 207 | 208 | # Guidance Automation Toolkit 209 | *.gpState 210 | 211 | # ReSharper is a .NET coding add-in 212 | _ReSharper*/ 213 | *.[Rr]e[Ss]harper 214 | *.DotSettings.user 215 | 216 | # TeamCity is a build add-in 217 | _TeamCity* 218 | 219 | # DotCover is a Code Coverage Tool 220 | *.dotCover 221 | 222 | # AxoCover is a Code Coverage Tool 223 | .axoCover/* 224 | !.axoCover/settings.json 225 | 226 | # Coverlet is a free, cross platform Code Coverage Tool 227 | coverage*.json 228 | coverage*.xml 229 | coverage*.info 230 | 231 | # Visual Studio code coverage results 232 | *.coverage 233 | *.coveragexml 234 | 235 | # NCrunch 236 | _NCrunch_* 237 | .*crunch*.local.xml 238 | nCrunchTemp_* 239 | 240 | # MightyMoose 241 | *.mm.* 242 | AutoTest.Net/ 243 | 244 | # Web workbench (sass) 245 | .sass-cache/ 246 | 247 | # Installshield output folder 248 | [Ee]xpress/ 249 | 250 | # DocProject is a documentation generator add-in 251 | DocProject/buildhelp/ 252 | DocProject/Help/*.HxT 253 | DocProject/Help/*.HxC 254 | DocProject/Help/*.hhc 255 | DocProject/Help/*.hhk 256 | DocProject/Help/*.hhp 257 | DocProject/Help/Html2 258 | DocProject/Help/html 259 | 260 | # Click-Once directory 261 | publish/ 262 | 263 | # Publish Web Output 264 | *.[Pp]ublish.xml 265 | *.azurePubxml 266 | # Note: Comment the next line if you want to checkin your web deploy settings, 267 | # but database connection strings (with potential passwords) will be unencrypted 268 | *.pubxml 269 | *.publishproj 270 | 271 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 272 | # checkin your Azure Web App publish settings, but sensitive information contained 273 | # in these scripts will be unencrypted 274 | PublishScripts/ 275 | 276 | # NuGet Packages 277 | *.nupkg 278 | # NuGet Symbol Packages 279 | *.snupkg 280 | # The packages folder can be ignored because of Package Restore 281 | **/[Pp]ackages/* 282 | # except build/, which is used as an MSBuild target. 283 | !**/[Pp]ackages/build/ 284 | # Uncomment if necessary however generally it will be regenerated when needed 285 | #!**/[Pp]ackages/repositories.config 286 | # NuGet v3's project.json files produces more ignorable files 287 | *.nuget.props 288 | *.nuget.targets 289 | 290 | # Nuget personal access tokens and Credentials 291 | # nuget.config 292 | 293 | # Microsoft Azure Build Output 294 | csx/ 295 | *.build.csdef 296 | 297 | # Microsoft Azure Emulator 298 | ecf/ 299 | rcf/ 300 | 301 | # Windows Store app package directories and files 302 | AppPackages/ 303 | BundleArtifacts/ 304 | Package.StoreAssociation.xml 305 | _pkginfo.txt 306 | *.appx 307 | *.appxbundle 308 | *.appxupload 309 | 310 | # Visual Studio cache files 311 | # files ending in .cache can be ignored 312 | *.[Cc]ache 313 | # but keep track of directories ending in .cache 314 | !?*.[Cc]ache/ 315 | 316 | # Others 317 | ClientBin/ 318 | ~$* 319 | *~ 320 | *.dbmdl 321 | *.dbproj.schemaview 322 | *.jfm 323 | *.pfx 324 | *.publishsettings 325 | orleans.codegen.cs 326 | 327 | # Including strong name files can present a security risk 328 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 329 | #*.snk 330 | 331 | # Since there are multiple workflows, uncomment next line to ignore bower_components 332 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 333 | #bower_components/ 334 | 335 | # RIA/Silverlight projects 336 | Generated_Code/ 337 | 338 | # Backup & report files from converting an old project file 339 | # to a newer Visual Studio version. Backup files are not needed, 340 | # because we have git ;-) 341 | _UpgradeReport_Files/ 342 | Backup*/ 343 | UpgradeLog*.XML 344 | UpgradeLog*.htm 345 | ServiceFabricBackup/ 346 | *.rptproj.bak 347 | 348 | # SQL Server files 349 | *.mdf 350 | *.ldf 351 | *.ndf 352 | 353 | # Business Intelligence projects 354 | *.rdl.data 355 | *.bim.layout 356 | *.bim_*.settings 357 | *.rptproj.rsuser 358 | *- [Bb]ackup.rdl 359 | *- [Bb]ackup ([0-9]).rdl 360 | *- [Bb]ackup ([0-9][0-9]).rdl 361 | 362 | # Microsoft Fakes 363 | FakesAssemblies/ 364 | 365 | # GhostDoc plugin setting file 366 | *.GhostDoc.xml 367 | 368 | # Node.js Tools for Visual Studio 369 | .ntvs_analysis.dat 370 | node_modules/ 371 | 372 | # Visual Studio 6 build log 373 | *.plg 374 | 375 | # Visual Studio 6 workspace options file 376 | *.opt 377 | 378 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 379 | *.vbw 380 | 381 | # Visual Studio LightSwitch build output 382 | **/*.HTMLClient/GeneratedArtifacts 383 | **/*.DesktopClient/GeneratedArtifacts 384 | **/*.DesktopClient/ModelManifest.xml 385 | **/*.Server/GeneratedArtifacts 386 | **/*.Server/ModelManifest.xml 387 | _Pvt_Extensions 388 | 389 | # Paket dependency manager 390 | .paket/paket.exe 391 | paket-files/ 392 | 393 | # FAKE - F# Make 394 | .fake/ 395 | 396 | # CodeRush personal settings 397 | .cr/personal 398 | 399 | # Python Tools for Visual Studio (PTVS) 400 | __pycache__/ 401 | *.pyc 402 | 403 | # Cake - Uncomment if you are using it 404 | # tools/** 405 | # !tools/packages.config 406 | 407 | # Tabs Studio 408 | *.tss 409 | 410 | # Telerik's JustMock configuration file 411 | *.jmconfig 412 | 413 | # BizTalk build output 414 | *.btp.cs 415 | *.btm.cs 416 | *.odx.cs 417 | *.xsd.cs 418 | 419 | # OpenCover UI analysis results 420 | OpenCover/ 421 | 422 | # Azure Stream Analytics local run output 423 | ASALocalRun/ 424 | 425 | # MSBuild Binary and Structured Log 426 | *.binlog 427 | 428 | # NVidia Nsight GPU debugger configuration file 429 | *.nvuser 430 | 431 | # MFractors (Xamarin productivity tool) working folder 432 | .mfractor/ 433 | 434 | # Local History for Visual Studio 435 | .localhistory/ 436 | 437 | # BeatPulse healthcheck temp database 438 | healthchecksdb 439 | 440 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 441 | MigrationBackup/ 442 | 443 | # Ionide (cross platform F# VS Code tools) working folder 444 | .ionide/ 445 | 446 | # Fody - auto-generated XML schema 447 | FodyWeavers.xsd 448 | 449 | # VS Code files for those working on multiple tools 450 | 451 | # Local History for Visual Studio Code 452 | 453 | # Windows Installer files from build outputs 454 | 455 | # JetBrains Rider 456 | .idea/ 457 | *.sln.iml 458 | 459 | ### VisualStudio Patch ### 460 | # Additional files built by Visual Studio 461 | 462 | # End of https://www.toptal.com/developers/gitignore/api/macos,windows,visualstudio,visualstudiocode 463 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "ms-dotnettools.csharp", 4 | "hbenl.vscode-test-explorer", 5 | "formulahendry.dotnet-test-explorer" 6 | ] 7 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "dotnet.defaultSolution": "slnup.slnx" 3 | } -------------------------------------------------------------------------------- /Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | $(MSBuildThisFileDirectory) 5 | true 6 | $(RepoRootPath)__artifacts 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Directory.Build.rsp: -------------------------------------------------------------------------------- 1 | /MaxCPUCount 2 | /NodeReuse:false -------------------------------------------------------------------------------- /Directory.Packages.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | true 9 | 10 | 11 | 12 | 22.0.14 13 | 14 | 15 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /Docs/BuildingAndTesting.md: -------------------------------------------------------------------------------- 1 | # Building and testing 2 | 3 | ## Requirements 4 | 5 | ### Using Visual Studio 6 | 7 | * [Visual Studio 2022 17.4+][download-vs] 8 | * You'll also need the [.NET 8 SDK][download-dotnet-8-sdk] and [.NET 9 SDK][download-dotnet-9-sdk]. 9 | 10 | ### Visual Studio Code 11 | 12 | * [Visual Studio Code][download-vs-code] 13 | * Install recommended extensions. 14 | * [.NET 8 SDK][download-dotnet-8-sdk] 15 | * [.NET 9 SDK][download-dotnet-9-sdk] 16 | 17 | ## Build the tool 18 | 19 | To build the tool, run the following command: 20 | 21 | ``` shell 22 | dotnet build -c Release 23 | ``` 24 | 25 | The tool will be packed into a `nupkg` file at `./__packages/NuGet/Release/`. 26 | 27 | ## Run the tests 28 | 29 | To run all the tests, simply run the following command: 30 | 31 | ``` shell 32 | dotnet test 33 | ``` 34 | 35 | ## Managing a tool installation from local build output 36 | 37 | ### Install the tool 38 | 39 | These instructions assume you have previously [built](#build-the-tool) the tool. 40 | 41 | To install the tool, run the following command: 42 | 43 | ``` shell 44 | dotnet tool install -g SlnUp --add-source ./__packages/NuGet/Release/ --version 45 | ``` 46 | 47 | ### Update the tool 48 | 49 | These instructions assume you have previously [built](#build-the-tool) and [installed](#install-the-tool) the tool. 50 | 51 | For stable release versions, run the following command: 52 | 53 | ``` shell 54 | dotnet tool update -g SlnUp --add-source ./__packages/NuGet/Release/ 55 | ``` 56 | 57 | For pre-release versions, you need to specify the `--prerelase` argument. 58 | 59 | ### Uninstall the tool 60 | 61 | These instructions assume you have previously [built](#build-the-tool) and [installed](#install-the-tool) the tool. 62 | 63 | To uninstall the tool, run the following command: 64 | 65 | ``` shell 66 | dotnet tool uninstall -g SlnUp 67 | ``` 68 | 69 | ## Updating Visual Studio versions 70 | 71 | The VisualStudio.VersionScraper tool is used to retrieve and update the Visual Studio versions bundled with SlnUp. 72 | 73 | Simply run the following PowerShell script: 74 | 75 | ```powershell 76 | ./scripts/UpdateVSVersions.ps1 77 | ``` 78 | 79 | [download-dotnet-8-sdk]: https://dotnet.microsoft.com/download/dotnet/8.0 "Download .NET 8.0" 80 | [download-dotnet-9-sdk]: https://dotnet.microsoft.com/download/dotnet/9.0 "Download .NET 9.0" 81 | [download-vs]: https://visualstudio.microsoft.com/downloads/ "Download Visual Studio" 82 | [download-vs-code]: https://code.visualstudio.com/Download "Download Visual Studio Code" 83 | -------------------------------------------------------------------------------- /Docs/README.md: -------------------------------------------------------------------------------- 1 | # Documentation 2 | 3 | * [Building And Testing](./BuildingAndTesting.md) 4 | * [Research](./Research.md) 5 | -------------------------------------------------------------------------------- /Docs/Research.md: -------------------------------------------------------------------------------- 1 | # Visual Studio solution updater research 2 | 3 | The goal of this research is to determine what's required to build a tool that 4 | allows you to easily update the Visual Studio version information in a solution 5 | file using a Visual Studio version number. 6 | 7 | ## Why? 8 | 9 | The Visual Studio selector tool, which determines which version of Visual Studio 10 | to open when you double-click a .sln file, uses the solution 11 | [file header](https://docs.microsoft.com/visualstudio/extensibility/internals/solution-dot-sln-file?view=vs-2022#file-header) 12 | to determine which version of Visual Studio to open. 13 | 14 | Updating a solution file with correct version information requires you to 15 | understand the solution file header and the Visual Studio version information. 16 | 17 | ## What to learn 18 | 19 | This research details the shape of solution files generated by various tools in 20 | order to understand what's required to handle updating the various versions of 21 | solution files. 22 | 23 | We'll also seek to understand the attributes of the Visual Studio version so 24 | that we can update solution files with them appropriately. 25 | 26 | ## The Visual Studio solution file header 27 | 28 | According to the [documentation](https://docs.microsoft.com/visualstudio/extensibility/internals/solution-dot-sln-file?view=vs-2022#file-header), 29 | the file header usually looks like this: 30 | 31 | ``` 32 | Microsoft Visual Studio Solution File, Format Version 12.00 33 | # Visual Studio Version 16 34 | VisualStudioVersion = 16.0.28701.123 35 | MinimumVisualStudioVersion = 10.0.40219.1 36 | ``` 37 | 38 | Standard header that defines the file format version. 39 | > `Microsoft Visual Studio Solution File, Format Version 12.00` 40 | 41 | The major version of Visual Studio that (most recently) saved this solution file. 42 | This information controls the version number in the solution icon. 43 | > `# Visual Studio Version 16` 44 | 45 | The full version of Visual Studio that (most recently) saved the solution file. 46 | If the solution file is saved by a newer version of Visual Studio that has the 47 | same major version, this value is not updated so as to lessen churn in the file. 48 | > `VisualStudioVersion = 16.0.28701.123` 49 | 50 | The minimum (oldest) version of Visual Studio that can open this solution file. 51 | > `MinimumVisualStudioVersion = 10.0.40219.1` 52 | 53 | This is accurate for Visual Studio 2019 and 2022. 54 | 55 | ### Visual Studio 2017 56 | 57 | Visual Studio 2017 has a slight variation of the file header, which is documented 58 | [here](https://docs.microsoft.com/visualstudio/extensibility/internals/solution-dot-sln-file?view=vs-2017#file-header). 59 | 60 | ``` 61 | Microsoft Visual Studio Solution File, Format Version 12.00 62 | # Visual Studio 15 63 | VisualStudioVersion = 15.0.26730.15 64 | MinimumVisualStudioVersion = 10.0.40219.1 65 | ``` 66 | 67 | Note the difference in the major version most recently saved: `# Visual Studio 15` 68 | vs `# Visual Studio Version 16` for Visual Studio 2019 and above. 69 | 70 | ## Common solution sources 71 | 72 | * Visual Studio templates 73 | * .NET CLI 74 | * [Visual Studio solution generator](https://microsoft.github.io/slngen/) 75 | (a.k.a. slngen) 76 | 77 | ### Generated by Visual Studio 2019 78 | 79 | This was generated by Visual Studio 2019 16.11.6 for a new C# .NET 5 console 80 | application: 81 | 82 | ``` 83 | 84 | Microsoft Visual Studio Solution File, Format Version 12.00 85 | # Visual Studio Version 16 86 | VisualStudioVersion = 16.0.31829.152 87 | MinimumVisualStudioVersion = 10.0.40219.1 88 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Generated", "Generated\Generated.csproj", "{0416C64E-01C4-4541-8305-A55FBFA0388C}" 89 | EndProject 90 | Global 91 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 92 | Debug|Any CPU = Debug|Any CPU 93 | Release|Any CPU = Release|Any CPU 94 | EndGlobalSection 95 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 96 | {0416C64E-01C4-4541-8305-A55FBFA0388C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 97 | {0416C64E-01C4-4541-8305-A55FBFA0388C}.Debug|Any CPU.Build.0 = Debug|Any CPU 98 | {0416C64E-01C4-4541-8305-A55FBFA0388C}.Release|Any CPU.ActiveCfg = Release|Any CPU 99 | {0416C64E-01C4-4541-8305-A55FBFA0388C}.Release|Any CPU.Build.0 = Release|Any CPU 100 | EndGlobalSection 101 | GlobalSection(SolutionProperties) = preSolution 102 | HideSolutionNode = FALSE 103 | EndGlobalSection 104 | GlobalSection(ExtensibilityGlobals) = postSolution 105 | SolutionGuid = {554E3E08-EE1C-47D0-B207-103EBACAB529} 106 | EndGlobalSection 107 | EndGlobal 108 | 109 | ``` 110 | 111 | The solution file contains all the expected header information. 112 | 113 | ### Generated by Visual Studio 2022 114 | 115 | This was generated by Visual Studio 2022 17.0 for a new C# .NET 6 console 116 | application. 117 | 118 | ``` 119 | 120 | Microsoft Visual Studio Solution File, Format Version 12.00 121 | # Visual Studio Version 17 122 | VisualStudioVersion = 17.0.31903.59 123 | MinimumVisualStudioVersion = 10.0.40219.1 124 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Generated", "Generated\Generated.csproj", "{391BD0B8-2B52-46AA-98D1-9D9EDCA9FA19}" 125 | EndProject 126 | Global 127 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 128 | Debug|Any CPU = Debug|Any CPU 129 | Release|Any CPU = Release|Any CPU 130 | EndGlobalSection 131 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 132 | {391BD0B8-2B52-46AA-98D1-9D9EDCA9FA19}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 133 | {391BD0B8-2B52-46AA-98D1-9D9EDCA9FA19}.Debug|Any CPU.Build.0 = Debug|Any CPU 134 | {391BD0B8-2B52-46AA-98D1-9D9EDCA9FA19}.Release|Any CPU.ActiveCfg = Release|Any CPU 135 | {391BD0B8-2B52-46AA-98D1-9D9EDCA9FA19}.Release|Any CPU.Build.0 = Release|Any CPU 136 | EndGlobalSection 137 | GlobalSection(SolutionProperties) = preSolution 138 | HideSolutionNode = FALSE 139 | EndGlobalSection 140 | GlobalSection(ExtensibilityGlobals) = postSolution 141 | SolutionGuid = {A2D44F49-9EB7-436B-8C84-67A937526801} 142 | EndGlobalSection 143 | EndGlobal 144 | 145 | ``` 146 | 147 | The solution file contains all the expected header information. 148 | 149 | ### Generated by .NET CLI 150 | 151 | This was generated by the .NET 6 SDK 6.0.100. 152 | 153 | ``` 154 | 155 | Microsoft Visual Studio Solution File, Format Version 12.00 156 | # Visual Studio Version 16 157 | VisualStudioVersion = 16.0.30114.105 158 | MinimumVisualStudioVersion = 10.0.40219.1 159 | Global 160 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 161 | Debug|Any CPU = Debug|Any CPU 162 | Release|Any CPU = Release|Any CPU 163 | EndGlobalSection 164 | GlobalSection(SolutionProperties) = preSolution 165 | HideSolutionNode = FALSE 166 | EndGlobalSection 167 | EndGlobal 168 | 169 | ``` 170 | 171 | The solution file contains all the expected header information. 172 | 173 | ### Generated by Visual Studio solution generator 174 | 175 | This was generated by the Visual Studio solution generator (slngen) 7.2.0. 176 | 177 | ``` 178 | Microsoft Visual Studio Solution File, Format Version 12.00 179 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Cli", "Cli\Cli.csproj", "{7BB74C71-F9D6-416F-97CB-DB2C5C904FD0}" 180 | EndProject 181 | Global 182 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 183 | Debug|Any CPU = Debug|Any CPU 184 | Release|Any CPU = Release|Any CPU 185 | EndGlobalSection 186 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 187 | {7BB74C71-F9D6-416F-97CB-DB2C5C904FD0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 188 | {7BB74C71-F9D6-416F-97CB-DB2C5C904FD0}.Debug|Any CPU.Build.0 = Debug|Any CPU 189 | {7BB74C71-F9D6-416F-97CB-DB2C5C904FD0}.Release|Any CPU.ActiveCfg = Release|Any CPU 190 | {7BB74C71-F9D6-416F-97CB-DB2C5C904FD0}.Release|Any CPU.Build.0 = Release|Any CPU 191 | EndGlobalSection 192 | GlobalSection(SolutionProperties) = preSolution 193 | HideSolutionNode = FALSE 194 | EndGlobalSection 195 | GlobalSection(ExtensibilityGlobals) = postSolution 196 | SolutionGuid = {1BB30EB4-5307-4751-A493-3746A4F75FCE} 197 | EndGlobalSection 198 | EndGlobal 199 | 200 | ``` 201 | 202 | The solution file only contains the file format version and is missing: 203 | 204 | * The major version comment that most recently saved the file. 205 | * The `VisualStudioVersion`. 206 | * The `MinimumVisualStudioVersion`. 207 | 208 | ## Visual Studio version information 209 | 210 | The components of a Visual Studio version appear to be a simple 3 part version 211 | and a 4-part build version. There's also a channel which defines the release 212 | channel a version belongs to: Release or Preview. 213 | 214 | For example, [Visual Studio 2022 version information](https://docs.microsoft.com/visualstudio/install/visual-studio-build-numbers-and-release-dates?view=vs-2022) 215 | looks like this: 216 | 217 | | Version | Build Version | Channel | 218 | | --- | --- | --- | 219 | | 17.0.1 | 17.1.31903.286 | Preview 1 | 220 | | 17.0.0 | 17.0.31903.59 | Release | 221 | | 17.0.0 | 17.0.31825.309 | Preview 7 | 222 | 223 | ## Requirements 224 | 225 | * Support solution files with file format version `12.00`. 226 | * Support solution files with all header information. 227 | * Support solution files without all header information by adding the missing 228 | information for the version specified. 229 | 230 | ## Out of scope 231 | 232 | * Generating new solution files. 233 | * Support for anything pre-Visual Studio 2017. 234 | 235 | ## Resources 236 | 237 | * [Visual Studio solution files](https://docs.microsoft.com/visualstudio/extensibility/internals/solution-dot-sln-file?view=vs-2022) 238 | 239 | ### Visual Studio version numbers 240 | 241 | * [Visual Studio 2017 build numbers and release dates](https://docs.microsoft.com/visualstudio/install/visual-studio-build-numbers-and-release-dates?view=vs-2017) 242 | * [Visual Studio 2019 build numbers and release dates](https://docs.microsoft.com/visualstudio/install/visual-studio-build-numbers-and-release-dates?view=vs-2019) 243 | * [Visual Studio 2022 build numbers and release dates](https://docs.microsoft.com/visualstudio/install/visual-studio-build-numbers-and-release-dates?view=vs-2022) 244 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Craig Treasure 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Visual Studio solution updater 2 | 3 | This is a [.NET tool](https://docs.microsoft.com/en-us/dotnet/core/tools/global-tools) 4 | that allows you to easily change the Visual Studio version information in a 5 | solution file using a Visual Studio version number. 6 | 7 | ![CI Build](https://github.com/craigktreasure/SlnUp/workflows/SlnUp-CI/badge.svg?branch=main) 8 | [![codecov](https://codecov.io/gh/craigktreasure/SlnUp/branch/main/graph/badge.svg?token=SV8DFLV2H4)](https://codecov.io/gh/craigktreasure/SlnUp) 9 | [![NuGet](https://img.shields.io/nuget/v/SlnUp)](https://www.nuget.org/packages/SlnUp/) 10 | [![NuGet](https://img.shields.io/nuget/dt/SlnUp)](https://www.nuget.org/packages/SlnUp/) 11 | 12 | - [Visual Studio solution updater](#visual-studio-solution-updater) 13 | - [Why?](#why) 14 | - [How to use it](#how-to-use-it) 15 | - [Install the tool](#install-the-tool) 16 | - [Update the tool](#update-the-tool) 17 | - [Run the tool](#run-the-tool) 18 | - [How does it work?](#how-does-it-work) 19 | - [What is supported?](#what-is-supported) 20 | 21 | ## Why? 22 | 23 | The Visual Studio Version Selector tool, which determines which version of 24 | Visual Studio to open when you double-click a .sln file, uses the solution 25 | [file header][vs-sln-file-header] to determine which version of Visual Studio 26 | to open. 27 | 28 | Updating a solution file with correct version information requires you to 29 | understand the solution file header and the Visual Studio version information. 30 | 31 | ## How to use it 32 | 33 | ### Install the tool 34 | 35 | ```shell 36 | dotnet tool install -g SlnUp 37 | ``` 38 | 39 | ### Update the tool 40 | 41 | ```shell 42 | dotnet tool update -g SlnUp 43 | ``` 44 | 45 | ### Run the tool 46 | 47 | To view all available options, you can run: 48 | 49 | ```shell 50 | slnup --help 51 | ``` 52 | 53 | The simplest form is to run the tool (`slnup`) from a directory containing a 54 | single solution (`.sln`) file. This will cause the tool to discover a solution 55 | file in the current directory and update it with the latest version. 56 | 57 | ```shell 58 | slnup 59 | ``` 60 | 61 | You can also pass a Visual Studio product year (2017, 2019, or 2022) to update 62 | to the latest version for the specified product year. 63 | 64 | ```shell 65 | slnup 2022 66 | ``` 67 | 68 | You can also specify a specific version of Visual Studio using a two-part 69 | (ex. 17.0) or three-part (17.1.0) version number: 70 | 71 | ```shell 72 | slnup 17.0 73 | ``` 74 | 75 | A path to a solution file may also be specified using the `-p` or `--path` 76 | parameters. This will be necessary if there is more than one solution file 77 | in the current directory. 78 | 79 | ```shell 80 | slnup 2022 --path ./path/to/solution.sln 81 | ``` 82 | 83 | If you want to specify the exact version information to be put into the solution 84 | file header, you can specify the version information like so: 85 | 86 | ```shell 87 | slnup 17.0 --build-version 17.0.31903.59 88 | ``` 89 | 90 | ## How does it work? 91 | 92 | Visual Studio solution files have a well known [file header][vs-sln-file-header]. 93 | The tool knows how to parse and update the solution file header and save the 94 | file. The tool also knows version information for builds of Visual Studio from 95 | Visual Studio 2017 to 2022. The version information specified, is used to update 96 | the file header. 97 | 98 | Consider a solution file with the following file header: 99 | 100 | ```text 101 | Microsoft Visual Studio Solution File, Format Version 12.00 102 | # Visual Studio Version 16 103 | VisualStudioVersion = 16.0.28701.123 104 | MinimumVisualStudioVersion = 10.0.40219.1 105 | ``` 106 | 107 | This solution file header was last updated using Visual Studio 16 (a.k.a. 108 | Visual Studio 2019) with build number `16.0.28701.123`. 109 | 110 | When you double click on a solution file like this, the Visual Studio Version 111 | Selector tool will attempt to locate an installed version of Visual Studio that 112 | most closely matches the version information. If Visual Studio 2019 is installed, 113 | that will likely be used to open the solution file. 114 | 115 | Now, let's say you want to update the solution file to be opened with Visual 116 | Studio 2022. You could run the following to update the solution file with 117 | version information for Visual Studio 2022: 118 | 119 | ```shell 120 | slnup 2022 121 | ``` 122 | 123 | In the case of `2022`, the tool will use the latest known version information 124 | it knows for that version. So, this would cause the tool to update the solution 125 | file header to the following: 126 | 127 | ```text 128 | Microsoft Visual Studio Solution File, Format Version 12.00 129 | # Visual Studio Version 17 130 | VisualStudioVersion = 17.0.31903.59 131 | MinimumVisualStudioVersion = 10.0.40219.1 132 | ``` 133 | 134 | Now, when you double click on the updated solution file, the Visual Studio 135 | Version Selector tool will attempt to locate and open the solution file using 136 | Visual Studio 2022. 137 | 138 | ## What is supported? 139 | 140 | - The tool supports updating solution files with file format version `12.00`. 141 | - If the solution file does not contain a file format version, you will 142 | receive an error. 143 | - If the solution file header does not contain version information such as 144 | `# Visual Studio Version 17`, `VisualStudioVersion = 17.0.31903.59`, or 145 | `MinimumVisualStudioVersion = 10.0.40219.1`, then those values will be added 146 | to the file header. 147 | - The tool supports Visual Studio 2017, 2019, and 2022. 148 | 149 | [vs-sln-file-header]: https://docs.microsoft.com/visualstudio/extensibility/internals/solution-dot-sln-file?view=vs-2022#file-header "File header" 150 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | status: 3 | project: 4 | default: 5 | target: 95% 6 | threshold: 1% 7 | -------------------------------------------------------------------------------- /eng/Defaults.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /eng/DotNetAnalyzers.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | true 8 | AllEnabledByDefault 9 | true 10 | 9.0 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /eng/DotNetDefaults.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | false 6 | 7 | true 8 | true 9 | 10 | 13 | latest 14 | 15 | 18 | enable 19 | 20 | 21 | enable 22 | 23 | 24 | true 25 | 26 | 27 | true 28 | 29 | 30 | embedded 31 | 32 | 33 | true 34 | 35 | 36 | $(IsCIBuild) 37 | 38 | 39 | 40 | 43 | 1701;1702 44 | 45 | 46 | 47 | true 48 | true 49 | 50 | 51 | 52 | 53 | $(ArtifactsPath)/test-results/ 54 | $(BaseTestResultsOutputPath)$(MSBuildProjectName)/ 55 | $(BaseProjectTestResultsOutputPath) 56 | $(BaseProjectTestResultsOutputPath) 57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /eng/Packaging.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8 | Craig Treasure 9 | craigktreasure 10 | Copyright © Craig Treasure 11 | https://github.com/craigktreasure/SlnUp 12 | https://github.com/craigktreasure/SlnUp.git 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /global.json: -------------------------------------------------------------------------------- 1 | { 2 | "sdk": { 3 | "version": "9.0.300", 4 | "allowPrerelease": false, 5 | "rollForward": "latestPatch" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /nuget.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /scripts/PatchGlobalJson.ps1: -------------------------------------------------------------------------------- 1 | if ($true) { 2 | return 3 | } 4 | 5 | Write-Host 'Disabling rollForward to pin to version in global.json.' 6 | $globalJsonPath = Join-Path $PSScriptRoot '../global.json' 7 | $globalJson = Get-Content $globalJsonPath -Raw | ConvertFrom-Json 8 | $globalJson.sdk.rollForward = 'disable' 9 | $globalJson | ConvertTo-Json | Set-Content -Path $globalJsonPath 10 | -------------------------------------------------------------------------------- /scripts/UpdateVSVersions.ps1: -------------------------------------------------------------------------------- 1 | #Requires -PSEdition Core 2 | 3 | $repoRoot = Join-Path $PSScriptRoot '..' 4 | 5 | Push-Location $repoRoot 6 | 7 | try { 8 | & dotnet run --project .\src\VisualStudio.VersionScraper\ -- .\src\SlnUp.Core\VersionManager.g.cs --format CSharp 9 | } 10 | finally { 11 | Pop-Location 12 | } 13 | -------------------------------------------------------------------------------- /slnup.slnx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /src/.editorconfig: -------------------------------------------------------------------------------- 1 | # Project specific settings 2 | 3 | [*.{cs,vb}] 4 | 5 | # CA1303: Do not pass literals as localized parameters 6 | dotnet_diagnostic.CA1303.severity = none 7 | 8 | # SYSLIB1045: Use GeneratedRegexAttribute to generate the regular expression implementation at compile time. 9 | # The GeneratedRegexAttribute is only available in .NET 7. 10 | dotnet_diagnostic.SYSLIB1045.severity = none 11 | -------------------------------------------------------------------------------- /src/Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | true 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/SlnUp.Core/ConsoleHelpers.cs: -------------------------------------------------------------------------------- 1 | namespace SlnUp.Core; 2 | 3 | using System; 4 | 5 | /// 6 | /// Class ConsoleHelpers. 7 | /// 8 | public static class ConsoleHelpers 9 | { 10 | /// 11 | /// Configures the console foreground color for errors for the lifetime of the . 12 | /// 13 | /// . 14 | public static IDisposable WithError() 15 | => WithForegroundColor(ConsoleColor.Red); 16 | 17 | /// 18 | /// Configures the console foreground color for the lifetime of the . 19 | /// 20 | /// The color. 21 | /// . 22 | public static IDisposable WithForegroundColor(ConsoleColor color) 23 | { 24 | ConsoleColor originalColor = Console.ForegroundColor; 25 | Console.ForegroundColor = color; 26 | 27 | return new ScopedAction(() => Console.ForegroundColor = originalColor); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/SlnUp.Core/Extensions/RegexExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace SlnUp.Core.Extensions; 2 | 3 | using System.Diagnostics.CodeAnalysis; 4 | using System.Text.RegularExpressions; 5 | 6 | /// 7 | /// Class RegexExtensions. 8 | /// 9 | public static class RegexExtensions 10 | { 11 | /// 12 | /// Tries to match the specified . 13 | /// 14 | /// The regex. 15 | /// The input. 16 | /// The match. 17 | /// true if the match was successful, false otherwise. 18 | public static bool TryMatch(this Regex regex, string input, [NotNullWhen(true)] out Match? match) 19 | { 20 | ArgumentNullException.ThrowIfNull(regex); 21 | 22 | match = null; 23 | Match myMatch = regex.Match(input); 24 | 25 | if (myMatch.Success) 26 | { 27 | match = myMatch; 28 | } 29 | 30 | return match is not null; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/SlnUp.Core/Extensions/VersionExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace SlnUp.Core.Extensions; 2 | 3 | using System; 4 | 5 | /// 6 | /// Class VersionExtensions. 7 | /// 8 | public static class VersionExtensions 9 | { 10 | /// 11 | /// Determines whether the versions have the same major and minor version numbers. 12 | /// 13 | /// The version. 14 | /// The other. 15 | /// true if the major and minor versions are the same; otherwise, false. 16 | public static bool HasSameMajorMinor(this Version version, Version other) 17 | { 18 | ArgumentNullException.ThrowIfNull(version); 19 | ArgumentNullException.ThrowIfNull(other); 20 | 21 | return version.Major == other.Major && version.Minor == other.Minor; 22 | } 23 | 24 | /// 25 | /// Determines if the version declares a 2-part version number. 26 | /// 27 | /// The version. 28 | /// true if the version declares a 2-part version number, false otherwise. 29 | public static bool Is2PartVersion(this Version version) 30 | { 31 | ArgumentNullException.ThrowIfNull(version); 32 | return version is { Major: >= 0, Minor: >= 0, Build: -1, Revision: -1 }; 33 | } 34 | 35 | /// 36 | /// Determines if the version declares a 3-part version number. 37 | /// 38 | /// The version. 39 | /// true if the version declares a 3-part version number, false otherwise. 40 | public static bool Is3PartVersion(this Version version) 41 | { 42 | ArgumentNullException.ThrowIfNull(version); 43 | return version is { Major: >= 0, Minor: >= 0, Build: >= 0, Revision: -1 }; 44 | } 45 | 46 | /// 47 | /// Determines if the version declares a 4-part version number. 48 | /// 49 | /// The version. 50 | /// true if the version declares a 4-part version number, false otherwise. 51 | public static bool Is4PartVersion(this Version version) 52 | { 53 | ArgumentNullException.ThrowIfNull(version); 54 | return version is { Major: >= 0, Minor: >= 0, Build: >= 0, Revision: >= 0 }; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/SlnUp.Core/ScopedAction.cs: -------------------------------------------------------------------------------- 1 | namespace SlnUp.Core; 2 | 3 | /// 4 | /// Class ScopedAction. 5 | /// Implements the 6 | /// 7 | /// 8 | public class ScopedAction : IDisposable 9 | { 10 | private readonly Action action; 11 | 12 | private bool disposedValue; 13 | 14 | /// 15 | /// Initializes a new instance of the class. 16 | /// 17 | /// The action. 18 | public ScopedAction(Action action) 19 | => this.action = action; 20 | 21 | /// 22 | /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. 23 | /// 24 | public void Dispose() 25 | { 26 | this.Dispose(disposing: true); 27 | GC.SuppressFinalize(this); 28 | } 29 | 30 | /// 31 | /// Releases unmanaged and - optionally - managed resources. 32 | /// 33 | /// 34 | /// true to release both managed and unmanaged resources; false to release only unmanaged resources. 35 | /// 36 | protected virtual void Dispose(bool disposing) 37 | { 38 | if (!this.disposedValue) 39 | { 40 | if (disposing) 41 | { 42 | this.action?.Invoke(); 43 | } 44 | 45 | this.disposedValue = true; 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/SlnUp.Core/SlnUp.Core.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net9.0;net8.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/SlnUp.Core/VersionManager.cs: -------------------------------------------------------------------------------- 1 | namespace SlnUp.Core; 2 | 3 | using System.Diagnostics.CodeAnalysis; 4 | 5 | using SlnUp.Core.Extensions; 6 | 7 | /// 8 | /// Manages versions of Visual Studio. 9 | /// 10 | public partial class VersionManager 11 | { 12 | private readonly IReadOnlyList versions; 13 | 14 | /// 15 | /// Initializes a new instance of the class. 16 | /// 17 | public VersionManager() 18 | : this(GetDefaultVersions()) 19 | { 20 | } 21 | 22 | /// 23 | /// Initializes a new instance of the class. 24 | /// 25 | /// The versions. 26 | public VersionManager(IReadOnlyList versions) 27 | => this.versions = versions; 28 | 29 | /// 30 | /// Tries to parse a Visual Studio version (x.x[.x]) from the specified input. 31 | /// 32 | /// The input. 33 | /// The version. 34 | /// true if a valid Visual Studio version was parsed, false otherwise. 35 | public static bool TryParseVisualStudioVersion(string? input, [NotNullWhen(true)] out Version? version) 36 | { 37 | version = null; 38 | 39 | if (string.IsNullOrWhiteSpace(input)) 40 | { 41 | return false; 42 | } 43 | 44 | if (Version.TryParse(input, out Version? potentialVersion) 45 | && (potentialVersion.Is2PartVersion() || potentialVersion.Is3PartVersion())) 46 | { 47 | version = potentialVersion; 48 | } 49 | 50 | return version is not null; 51 | } 52 | 53 | /// 54 | /// Gets a Visual Studio version from the specified input. 55 | /// Supported inputs are a 2 or 3-part version number or a valid product year. 56 | /// 57 | /// The version input. 58 | /// . 59 | public VisualStudioVersion? FromVersionParameter(string? versionInput) 60 | { 61 | if (versionInput is null) 62 | { 63 | return null; 64 | } 65 | 66 | if (TryParseVisualStudioVersion(versionInput, out Version? parsedVersion)) 67 | { 68 | return this.FindLatestMatchingVersion(parsedVersion); 69 | } 70 | else if (TryParseVisualStudioProduct(versionInput, out VisualStudioProduct product)) 71 | { 72 | return this.FindLatestMatchingVersion(product); 73 | } 74 | 75 | return null; 76 | } 77 | 78 | private static VisualStudioProduct GetVersionFromProductYear(int productYear) 79 | => productYear switch 80 | { 81 | 2017 => VisualStudioProduct.VisualStudio2017, 82 | 2019 => VisualStudioProduct.VisualStudio2019, 83 | 2022 => VisualStudioProduct.VisualStudio2022, 84 | _ => VisualStudioProduct.Unknown, 85 | }; 86 | 87 | private static bool TryParseVisualStudioProduct(string input, out VisualStudioProduct product) 88 | { 89 | product = VisualStudioProduct.Unknown; 90 | 91 | if (int.TryParse(input, out int productYear)) 92 | { 93 | product = GetVersionFromProductYear(productYear); 94 | } 95 | 96 | return product is not VisualStudioProduct.Unknown; 97 | } 98 | 99 | private VisualStudioVersion? FindLatestMatchingVersion(Version version) 100 | { 101 | VisualStudioVersion? versionResolved = null; 102 | 103 | if (version.Is3PartVersion()) 104 | { 105 | // Expect an exact match. 106 | versionResolved = this.versions.FirstOrDefault(v => v.Version == version); 107 | } 108 | 109 | if (version.Is2PartVersion()) 110 | { 111 | // Expect a major.minor match. 112 | versionResolved = this.versions.Where(v => v.Version.HasSameMajorMinor(version)).MaxBy(v => v.BuildVersion); 113 | } 114 | 115 | return versionResolved; 116 | } 117 | 118 | private VisualStudioVersion? FindLatestMatchingVersion(VisualStudioProduct product) 119 | => this.versions.Where(v => v.Product == product).MaxBy(v => v.BuildVersion); 120 | } 121 | -------------------------------------------------------------------------------- /src/SlnUp.Core/VisualStudioProduct.cs: -------------------------------------------------------------------------------- 1 | namespace SlnUp.Core; 2 | 3 | /// 4 | /// Represents a Visual Studio product 5 | /// 6 | public enum VisualStudioProduct 7 | { 8 | /// 9 | /// The product is unknown. 10 | /// 11 | Unknown = 0, 12 | 13 | /// 14 | /// The Visual Studio 2017 product. 15 | /// 16 | VisualStudio2017 = 2017, 17 | 18 | /// 19 | /// The Visual Studio 2019 product. 20 | /// 21 | VisualStudio2019 = 2019, 22 | 23 | /// 24 | /// The Visual Studio 2022 product. 25 | /// 26 | VisualStudio2022 = 2022, 27 | } 28 | -------------------------------------------------------------------------------- /src/SlnUp.Core/VisualStudioVersion.cs: -------------------------------------------------------------------------------- 1 | namespace SlnUp.Core; 2 | 3 | using System.Text; 4 | 5 | using Humanizer; 6 | 7 | /// 8 | /// Represents Visual Studio version details. 9 | /// 10 | public class VisualStudioVersion : IEquatable 11 | { 12 | /// 13 | /// Gets the Visual Studio build version. 14 | /// 15 | public Version BuildVersion { get; init; } 16 | 17 | /// 18 | /// Gets the Visual Studio channel. 19 | /// 20 | public string Channel { get; init; } 21 | 22 | /// 23 | /// Gets the Visual Studio full product title. 24 | /// 25 | public string FullProductTitle => this.ToString(); 26 | 27 | /// 28 | /// Gets a value indicating whether the Visual Studio version is a preview. 29 | /// 30 | public bool IsPreview { get; init; } 31 | 32 | /// 33 | /// Gets the Visual Studio product. 34 | /// 35 | public VisualStudioProduct Product { get; init; } 36 | 37 | /// 38 | /// Gets the Visual Studio product title. 39 | /// 40 | public string ProductTitle => this.Product.Humanize().Transform(To.TitleCase); 41 | 42 | /// 43 | /// Gets the Visual Studio version. 44 | /// 45 | public Version Version { get; init; } 46 | 47 | /// 48 | /// Initializes a new instance of the class. 49 | /// 50 | /// The Visual Studio product. 51 | /// The Visual Studio version. 52 | /// The Visual Studio build version. 53 | /// The Visual Studio channel. 54 | /// if set to true, the Visual Studio version is a preview. 55 | public VisualStudioVersion( 56 | VisualStudioProduct product, 57 | Version version, 58 | Version buildVersion, 59 | string channel = "", 60 | bool isPreview = false) 61 | { 62 | this.Product = product; 63 | this.Version = version; 64 | this.BuildVersion = buildVersion; 65 | this.Channel = channel; 66 | this.IsPreview = isPreview; 67 | } 68 | 69 | /// 70 | /// Indicates whether the current object is equal to another object of the same type. 71 | /// 72 | /// An object to compare with this object. 73 | /// if the current object is equal to the parameter; otherwise, . 74 | public bool Equals(VisualStudioVersion? other) 75 | => other is not null && this.BuildVersion == other.BuildVersion; 76 | 77 | /// 78 | /// Determines whether the specified is equal to this instance. 79 | /// 80 | /// The object to compare with the current object. 81 | /// true if the specified is equal to this instance; otherwise, false. 82 | public override bool Equals(object? obj) 83 | => obj is VisualStudioVersion visualStudioVersion && this.Equals(visualStudioVersion); 84 | 85 | /// 86 | /// Returns a hash code for this instance. 87 | /// 88 | /// A hash code for this instance, suitable for use in hashing algorithms and data structures like a hash table. 89 | public override int GetHashCode() => this.BuildVersion.GetHashCode(); 90 | 91 | /// 92 | /// Returns a that represents this instance. 93 | /// 94 | /// A that represents this instance. 95 | public override string ToString() 96 | { 97 | StringBuilder sb = new(this.ProductTitle); 98 | 99 | if (this.IsPreview) 100 | { 101 | sb.Append(' ').Append(this.Version); 102 | 103 | if (!string.IsNullOrWhiteSpace(this.Channel)) 104 | { 105 | sb.Append(' ').Append(this.Channel); 106 | } 107 | } 108 | else 109 | { 110 | sb.Append(' ') 111 | .Append(this.Version.Major) 112 | .Append('.') 113 | .Append(this.Version.Minor); 114 | } 115 | 116 | return sb.ToString(); 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/SlnUp.Json/Extensions/AssemblyExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace SlnUp.Json.Extensions; 2 | 3 | using System; 4 | using System.Reflection; 5 | 6 | internal static class AssemblyExtensions 7 | { 8 | /// 9 | /// Gets the content of the embedded file resource. 10 | /// 11 | /// The assembly. 12 | /// The file path. 13 | /// . 14 | /// Resource not found in assembly - filePath 15 | public static string GetEmbeddedFileResourceContent(this Assembly assembly, string filePath) 16 | { 17 | using Stream stream = assembly.GetManifestResourceStream(filePath) 18 | ?? throw new ArgumentException("Resource not found in assembly", nameof(filePath)); 19 | using StreamReader streamReader = new(stream); 20 | return streamReader.ReadToEnd(); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/SlnUp.Json/SlnUp.Json.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net9.0;net8.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/SlnUp.Json/VersionManagerJsonHelper.cs: -------------------------------------------------------------------------------- 1 | namespace SlnUp.Json; 2 | 3 | using System.Reflection; 4 | 5 | using SlnUp.Core; 6 | using SlnUp.Json.Extensions; 7 | 8 | /// 9 | /// A helper when loading a from json. 10 | /// 11 | public static class VersionManagerJsonHelper 12 | { 13 | /// 14 | /// Loads a from an embedded file resource. 15 | /// 16 | /// The assembly. 17 | /// The file path. 18 | /// . 19 | public static VersionManager LoadFromEmbeddedResource(Assembly assembly, string filePath) 20 | { 21 | ArgumentNullException.ThrowIfNull(assembly); 22 | 23 | string jsonContent = assembly.GetEmbeddedFileResourceContent(filePath); 24 | 25 | IReadOnlyList versions = VisualStudioVersionJsonHelper.FromJson(jsonContent); 26 | 27 | return new VersionManager(versions); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/SlnUp.Json/VisualStudioVersionJsonHelper.cs: -------------------------------------------------------------------------------- 1 | namespace SlnUp.Json; 2 | 3 | using System.IO.Abstractions; 4 | using System.Text.Json; 5 | using System.Text.Json.Serialization; 6 | 7 | using SlnUp.Core; 8 | 9 | /// 10 | /// A helper when converting data to and from json. 11 | /// 12 | public static class VisualStudioVersionJsonHelper 13 | { 14 | private static readonly JsonSerializerOptions serializerOptions = new() 15 | { 16 | IgnoreReadOnlyProperties = true, 17 | WriteIndented = true, 18 | Converters = 19 | { 20 | new JsonStringEnumConverter(JsonNamingPolicy.CamelCase) 21 | } 22 | }; 23 | 24 | /// 25 | /// Deserializes version details from json content. 26 | /// 27 | /// The json. 28 | /// . 29 | /// The file did not contain valid json. 30 | public static IReadOnlyList FromJson(string json) 31 | => JsonSerializer.Deserialize>(json, serializerOptions) 32 | ?? throw new InvalidDataException("The file did not contain valid json for version details."); 33 | 34 | /// 35 | /// Loads from json. 36 | /// 37 | /// The file system. 38 | /// The file path. 39 | /// . 40 | public static IReadOnlyList FromJsonFile(IFileSystem fileSystem, string filePath) 41 | { 42 | ArgumentNullException.ThrowIfNull(fileSystem); 43 | 44 | return FromJson(fileSystem.File.ReadAllText(filePath)); 45 | } 46 | 47 | /// 48 | /// Serializes to json content. 49 | /// 50 | /// The versions. 51 | public static string ToJson(IEnumerable versions) 52 | => JsonSerializer.Serialize(versions, serializerOptions); 53 | 54 | /// 55 | /// Saves to json. 56 | /// 57 | /// The file system. 58 | /// The versions. 59 | /// The file path. 60 | public static void ToJsonFile(IFileSystem fileSystem, IEnumerable versions, string filePath) 61 | { 62 | ArgumentNullException.ThrowIfNull(fileSystem); 63 | 64 | fileSystem.File.WriteAllText(filePath, ToJson(versions)); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/SlnUp/CLI/ArgumentParser.cs: -------------------------------------------------------------------------------- 1 | namespace SlnUp.CLI; 2 | 3 | using System.CommandLine.Parsing; 4 | using System.Globalization; 5 | 6 | internal static class ArgumentParser 7 | { 8 | private const string CannotParseArgumentOption = "Cannot parse argument '{0}' for option '{1}' as expected type '{2}'."; 9 | 10 | #if NET8_0_OR_GREATER 11 | private static readonly System.Text.CompositeFormat CannotParseArgumentOptionFormat = System.Text.CompositeFormat.Parse(CannotParseArgumentOption); 12 | #endif 13 | 14 | public static Version? ParseVersion(ArgumentResult result) 15 | { 16 | string tokenValue = result.Tokens.Single().Value; 17 | if (Version.TryParse(tokenValue, out Version? version)) 18 | { 19 | return version; 20 | } 21 | 22 | result.ErrorMessage = string.Format( 23 | CultureInfo.InvariantCulture, 24 | #if NET8_0_OR_GREATER 25 | CannotParseArgumentOptionFormat, 26 | #else 27 | CannotParseArgumentOption, 28 | #endif 29 | tokenValue, 30 | result.Argument.Name, 31 | result.Argument.ValueType); 32 | return null; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/SlnUp/CLI/ProgramOptions.cs: -------------------------------------------------------------------------------- 1 | namespace SlnUp.CLI; 2 | 3 | using System.CommandLine; 4 | using System.CommandLine.NamingConventionBinder; 5 | using System.Diagnostics.CodeAnalysis; 6 | using System.IO.Abstractions; 7 | 8 | using SlnUp; 9 | using SlnUp.Core; 10 | using SlnUp.Core.Extensions; 11 | 12 | internal class ProgramOptions 13 | { 14 | public static string DefaultVersionArgument => VersionManager.LatestProductValue; 15 | 16 | public Version? BuildVersion { get; set; } 17 | 18 | public string? Path { get; set; } 19 | 20 | public string? Version { get; set; } 21 | 22 | public static RootCommand Configure(Func invokeAction) 23 | { 24 | Option pathOption = new( 25 | name: "--path", 26 | description: "The path to the solution file."); 27 | pathOption.AddAlias("-p"); 28 | 29 | Argument versionArgument = new( 30 | name: "version", 31 | getDefaultValue: () => DefaultVersionArgument, 32 | description: "The Visual Studio version to update the solution file with. Can be either a product year (ex. 2017, 2019, or 2022) or a 2 or 3-part version number (ex. 16.9 or 17.0.1)."); 33 | 34 | Option buildVersionOption = new( 35 | name: "--build-version", 36 | description: "Uses version information as specified with this build version number.", 37 | parseArgument: ArgumentParser.ParseVersion); 38 | 39 | const string version = ThisAssembly.IsPrerelease 40 | ? ThisAssembly.AssemblyInformationalVersion 41 | : ThisAssembly.AssemblyFileVersion; 42 | RootCommand rootCommand = new($"SlnUp {version}") 43 | { 44 | pathOption, 45 | buildVersionOption, 46 | versionArgument 47 | }; 48 | 49 | rootCommand.Handler = CommandHandler.Create((ProgramOptions options) => 50 | { 51 | return Task.FromResult(invokeAction(options)); 52 | }); 53 | 54 | return rootCommand; 55 | } 56 | 57 | /// 58 | /// Tries to get . 59 | /// 60 | /// The file system. 61 | /// The options. 62 | /// true if the options were retrieved, false otherwise. 63 | public bool TryGetSlnUpOptions(IFileSystem fileSystem, [NotNullWhen(true)] out SlnUpOptions? options) 64 | { 65 | options = null; 66 | 67 | if (!TryResolveSolutionFilePath(fileSystem, this.Path, out string? solutionFilePath)) 68 | { 69 | return false; 70 | } 71 | 72 | if (!fileSystem.File.Exists(solutionFilePath)) 73 | { 74 | using IDisposable _ = ConsoleHelpers.WithError(); 75 | Console.WriteLine($"The solution file could not be found: '{solutionFilePath}'."); 76 | return false; 77 | } 78 | 79 | if (this.BuildVersion is not null) 80 | { 81 | // Use version information as provided. 82 | if (!this.BuildVersion.Is4PartVersion()) 83 | { 84 | using IDisposable _ = ConsoleHelpers.WithError(); 85 | Console.WriteLine("The build version must be a full 4-part version number."); 86 | return false; 87 | } 88 | 89 | if (!VersionManager.TryParseVisualStudioVersion(this.Version, out Version? version)) 90 | { 91 | using IDisposable _ = ConsoleHelpers.WithError(); 92 | Console.WriteLine("The version specified was not a valid 2 or 3-part version number."); 93 | return false; 94 | } 95 | 96 | options = new SlnUpOptions(solutionFilePath, version, this.BuildVersion); 97 | } 98 | else 99 | { 100 | // Try to lookup the version specified. 101 | VersionManager versionManager = new(); 102 | 103 | VisualStudioVersion? version = versionManager.FromVersionParameter(this.Version); 104 | 105 | if (version is null) 106 | { 107 | using IDisposable _ = ConsoleHelpers.WithError(); 108 | Console.WriteLine($"The version specified could not be resolved to a supported Visual Studio product version: '{this.Version}'."); 109 | return false; 110 | } 111 | 112 | options = new SlnUpOptions(solutionFilePath, version.Version, version.BuildVersion); 113 | } 114 | 115 | return true; 116 | } 117 | 118 | private static bool TryResolveSolutionFilePath(IFileSystem fileSystem, string? input, [NotNullWhen(true)] out string? solutionFilePath) 119 | { 120 | solutionFilePath = null; 121 | string currentDirectory = fileSystem.Directory.GetCurrentDirectory(); 122 | 123 | if (input is null) 124 | { 125 | string[] solutionFiles = [.. fileSystem.Directory.EnumerateFiles( 126 | currentDirectory, 127 | "*.sln", 128 | SearchOption.TopDirectoryOnly)]; 129 | 130 | if (solutionFiles.Length is 0) 131 | { 132 | using IDisposable _ = ConsoleHelpers.WithError(); 133 | 134 | Console.WriteLine("No solution (.sln) files could be found in the current directory."); 135 | 136 | return false; 137 | } 138 | 139 | if (solutionFiles.Length > 1) 140 | { 141 | using IDisposable _ = ConsoleHelpers.WithError(); 142 | 143 | Console.WriteLine("More than one solution (.sln) files were found in the current directory. Use the -p or --path paramter to specify one:"); 144 | foreach (string solutionFile in solutionFiles) 145 | { 146 | Console.WriteLine($" - '{solutionFile}'"); 147 | } 148 | 149 | return false; 150 | } 151 | 152 | solutionFilePath = solutionFiles[0]; 153 | } 154 | else 155 | { 156 | if (fileSystem.Path.IsPathFullyQualified(input)) 157 | { 158 | solutionFilePath = input; 159 | } 160 | else 161 | { 162 | // Assume it is a relative path to the current directory. 163 | solutionFilePath = fileSystem.Path.GetFullPath(input, currentDirectory); 164 | } 165 | } 166 | 167 | return solutionFilePath is not null; 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /src/SlnUp/Program.cs: -------------------------------------------------------------------------------- 1 | namespace SlnUp; 2 | 3 | using System.CommandLine; 4 | using System.Diagnostics.CodeAnalysis; 5 | using System.IO.Abstractions; 6 | 7 | using SlnUp.CLI; 8 | using SlnUp.Core; 9 | 10 | internal static class Program 11 | { 12 | public static int Main(string[] args) 13 | { 14 | Console.Title = ThisAssembly.AssemblyTitle; 15 | 16 | return ProgramOptions.Configure(Run).Invoke(args); 17 | } 18 | 19 | private static int Run(ProgramOptions options) 20 | => Run(new FileSystem(), options); 21 | 22 | private static int Run(IFileSystem fileSystem, ProgramOptions programOptions) 23 | { 24 | if (!programOptions.TryGetSlnUpOptions(fileSystem, out SlnUpOptions? options)) 25 | { 26 | return 1; 27 | } 28 | 29 | return Run(fileSystem, options); 30 | } 31 | 32 | [SuppressMessage("Design", "CA1031:Do not catch general exception types", Justification = "By design.")] 33 | private static int Run(IFileSystem fileSystem, SlnUpOptions options) 34 | { 35 | try 36 | { 37 | SolutionFile solutionFile = new(fileSystem, options.SolutionFilePath); 38 | solutionFile.UpdateFileHeader(options.BuildVersion); 39 | } 40 | catch (Exception ex) 41 | { 42 | using IDisposable _ = ConsoleHelpers.WithError(); 43 | Console.WriteLine(ex.Message); 44 | return 1; 45 | } 46 | 47 | return 0; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/SlnUp/SlnUp.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net9.0;net8.0 6 | 7 | true 8 | slnup 9 | true 10 | A tool for updating Visual Studio version information in solution files. 11 | README.md 12 | MIT 13 | false 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /src/SlnUp/SlnUpOptions.cs: -------------------------------------------------------------------------------- 1 | namespace SlnUp; 2 | 3 | using System; 4 | using System.Diagnostics.CodeAnalysis; 5 | 6 | /// 7 | /// Class SlnUpOptions. 8 | /// Implements the 9 | /// 10 | /// 11 | [ExcludeFromCodeCoverage] 12 | internal record SlnUpOptions(string SolutionFilePath, Version Version, Version BuildVersion); 13 | -------------------------------------------------------------------------------- /src/SlnUp/SolutionFile.cs: -------------------------------------------------------------------------------- 1 | namespace SlnUp; 2 | 3 | using System.Diagnostics.CodeAnalysis; 4 | using System.IO.Abstractions; 5 | using System.Text.RegularExpressions; 6 | 7 | using SlnUp.Core.Extensions; 8 | 9 | internal class SolutionFile 10 | { 11 | private static readonly Regex fileFormatVersionRegex = new( 12 | @"^Microsoft Visual Studio Solution File, Format Version (\d+\.\d+)$", 13 | RegexOptions.Compiled); 14 | 15 | private static readonly Regex lastVisualStudioMajorVersionRegex = new( 16 | @"^# Visual Studio(?: Version)? (\d+)$", 17 | RegexOptions.Compiled); 18 | 19 | private static readonly Regex lastVisualStudioVersionRegex = new( 20 | @"^VisualStudioVersion = (\d+\.\d+\.\d+\.\d+)$", 21 | RegexOptions.Compiled); 22 | 23 | private static readonly Regex minimumVisualStudioVersionRegex = new( 24 | @"^MinimumVisualStudioVersion = (\d+\.\d+\.\d+\.\d+)$", 25 | RegexOptions.Compiled); 26 | 27 | private readonly string filePath; 28 | 29 | private readonly IFileSystem fileSystem; 30 | 31 | private int fileFormatLineNumber = -1; 32 | 33 | /// 34 | /// Gets the file header. 35 | /// 36 | public SolutionFileHeader FileHeader { get; private set; } 37 | 38 | /// 39 | /// Initializes a new instance of the class. 40 | /// 41 | /// The file system. 42 | /// The file path. 43 | /// The solution file could not be found. 44 | public SolutionFile(IFileSystem fileSystem, string filePath) 45 | { 46 | if (!fileSystem.File.Exists(filePath)) 47 | { 48 | throw new FileNotFoundException("The solution file could not be found.", filePath); 49 | } 50 | 51 | this.fileSystem = fileSystem; 52 | this.filePath = filePath; 53 | 54 | this.FileHeader = this.LoadFileHeader(); 55 | } 56 | 57 | /// 58 | /// Reads the file content. 59 | /// 60 | /// . 61 | public string ReadContent() => this.fileSystem.File.ReadAllText(this.filePath); 62 | 63 | /// 64 | /// Reload. 65 | /// 66 | public void Reload() 67 | => this.FileHeader = this.LoadFileHeader(); 68 | 69 | /// 70 | /// Updates the file header. 71 | /// 72 | /// The new Visual Studio version. 73 | public void UpdateFileHeader(Version newVisualStudioVersion) 74 | { 75 | SolutionFileHeader newHeader = this.FileHeader.DuplicateAndUpdate(newVisualStudioVersion); 76 | this.UpdateFileHeader(newHeader); 77 | } 78 | 79 | internal void UpdateFileHeader(SolutionFileHeader fileHeader) 80 | { 81 | if (fileHeader.LastVisualStudioMajorVersion is null) 82 | { 83 | throw new InvalidDataException($"The {nameof(SolutionFileHeader.LastVisualStudioMajorVersion)} cannot be null."); 84 | } 85 | 86 | if (fileHeader.LastVisualStudioVersion is null) 87 | { 88 | throw new InvalidDataException($"The {nameof(SolutionFileHeader.LastVisualStudioVersion)} cannot be null."); 89 | } 90 | 91 | if (fileHeader.MinimumVisualStudioVersion is null) 92 | { 93 | // Inject a default minimum Visual Studio version. 94 | fileHeader = fileHeader with 95 | { 96 | MinimumVisualStudioVersion = Version.Parse(SolutionFileHeader.DefaultMinimumVisualStudioVersion) 97 | }; 98 | } 99 | 100 | List lines = [.. this.fileSystem.File.ReadAllLines(this.filePath)]; 101 | 102 | if (this.FileHeader.LastVisualStudioMajorVersion is null) 103 | { 104 | lines.Insert(this.fileFormatLineNumber + 1, string.Empty); 105 | } 106 | 107 | if (this.FileHeader.LastVisualStudioVersion is null) 108 | { 109 | lines.Insert(this.fileFormatLineNumber + 2, string.Empty); 110 | } 111 | 112 | if (this.FileHeader.MinimumVisualStudioVersion is null) 113 | { 114 | lines.Insert(this.fileFormatLineNumber + 3, string.Empty); 115 | } 116 | 117 | string fileFormatVersionLine = $"Microsoft Visual Studio Solution File, Format Version {fileHeader.FileFormatVersion}"; 118 | lines[this.fileFormatLineNumber] = fileFormatVersionLine; 119 | 120 | string lastVisualStudioMajorVersionLine = fileHeader.LastVisualStudioMajorVersion >= 16 121 | ? $"# Visual Studio Version {fileHeader.LastVisualStudioMajorVersion}" 122 | : $"# Visual Studio {fileHeader.LastVisualStudioMajorVersion}"; 123 | 124 | lines[this.fileFormatLineNumber + 1] = lastVisualStudioMajorVersionLine; 125 | 126 | string lastVisualStudioVersionLine = $"VisualStudioVersion = {fileHeader.LastVisualStudioVersion}"; 127 | lines[this.fileFormatLineNumber + 2] = lastVisualStudioVersionLine; 128 | 129 | string minimumVisualStudioVersionLine = $"MinimumVisualStudioVersion = {fileHeader.MinimumVisualStudioVersion}"; 130 | lines[this.fileFormatLineNumber + 3] = minimumVisualStudioVersionLine; 131 | 132 | this.fileSystem.File.WriteAllLines(this.filePath, lines); 133 | 134 | this.FileHeader = fileHeader; 135 | } 136 | 137 | private static bool TryGetFileFormat(string line, [NotNullWhen(true)] out string? fileFormatVersion) 138 | { 139 | fileFormatVersion = null; 140 | 141 | if (fileFormatVersionRegex.TryMatch(line, out Match? fileFormatVersionMatch)) 142 | { 143 | fileFormatVersion = fileFormatVersionMatch.Groups[1].Value; 144 | } 145 | 146 | return fileFormatVersion is not null; 147 | } 148 | 149 | private static bool TryGetLastVisualStudioMajorVersion(string line, [NotNullWhen(true)] out int? lastMajorVersion) 150 | { 151 | lastMajorVersion = null; 152 | 153 | if (lastVisualStudioMajorVersionRegex.TryMatch(line, out Match? majorVersionMatch) 154 | && int.TryParse(majorVersionMatch.Groups[1].Value, out int parsedLastMajorVersion)) 155 | { 156 | lastMajorVersion = parsedLastMajorVersion; 157 | } 158 | 159 | return lastMajorVersion is not null; 160 | } 161 | 162 | private static bool TryGetLastVisualStudioVersion(string line, [NotNullWhen(true)] out Version? lastVersion) 163 | { 164 | lastVersion = null; 165 | 166 | if (lastVisualStudioVersionRegex.TryMatch(line, out Match? majorVersionMatch) 167 | && Version.TryParse(majorVersionMatch.Groups[1].Value, out Version? parsedLastVersion)) 168 | { 169 | lastVersion = parsedLastVersion; 170 | } 171 | 172 | return lastVersion is not null; 173 | } 174 | 175 | private static bool TryGetMinimumVisualStudioVersion(string line, [NotNullWhen(true)] out Version? minimumVersion) 176 | { 177 | minimumVersion = null; 178 | 179 | if (minimumVisualStudioVersionRegex.TryMatch(line, out Match? minimumVersionMatch) 180 | && Version.TryParse(minimumVersionMatch.Groups[1].Value, out Version? parsedMinimumVersion)) 181 | { 182 | minimumVersion = parsedMinimumVersion; 183 | } 184 | 185 | return minimumVersion is not null; 186 | } 187 | 188 | private static bool TryLocateFileFormatLine( 189 | string[] lines, 190 | out int fileFormatLineNumber, 191 | [NotNullWhen(true)] out string? fileFormatVersion) 192 | { 193 | fileFormatLineNumber = -1; 194 | fileFormatVersion = null; 195 | 196 | for (int i = 0; i < lines.Length; ++i) 197 | { 198 | if (TryGetFileFormat(lines[i], out fileFormatVersion)) 199 | { 200 | fileFormatLineNumber = i; 201 | break; 202 | } 203 | } 204 | 205 | return fileFormatLineNumber != -1; 206 | } 207 | 208 | private SolutionFileHeader LoadFileHeader() 209 | { 210 | string[] lines = this.fileSystem.File.ReadAllLines(this.filePath); 211 | 212 | if (!TryLocateFileFormatLine(lines, out this.fileFormatLineNumber, out string? fileFormatVersion)) 213 | { 214 | throw new InvalidDataException($"The file does not contain a valid file format: '{this.filePath}'."); 215 | } 216 | 217 | SolutionFileHeader fileHeader = new(fileFormatVersion); 218 | 219 | int nextLine = this.fileFormatLineNumber + 1; 220 | if (lines.Length > nextLine + 1 221 | && TryGetLastVisualStudioMajorVersion(lines[nextLine], out int? lastMajorVersion)) 222 | { 223 | fileHeader = fileHeader with 224 | { 225 | LastVisualStudioMajorVersion = lastMajorVersion, 226 | }; 227 | ++nextLine; 228 | } 229 | 230 | if (lines.Length > nextLine + 1 231 | && TryGetLastVisualStudioVersion(lines[nextLine], out Version? lastVersion)) 232 | { 233 | fileHeader = fileHeader with 234 | { 235 | LastVisualStudioVersion = lastVersion, 236 | }; 237 | ++nextLine; 238 | } 239 | 240 | if (lines.Length > nextLine + 1 241 | && TryGetMinimumVisualStudioVersion(lines[nextLine], out Version? minimumVersion)) 242 | { 243 | fileHeader = fileHeader with 244 | { 245 | MinimumVisualStudioVersion = minimumVersion, 246 | }; 247 | } 248 | 249 | return fileHeader; 250 | } 251 | } 252 | -------------------------------------------------------------------------------- /src/SlnUp/SolutionFileHeader.cs: -------------------------------------------------------------------------------- 1 | namespace SlnUp; 2 | 3 | internal record SolutionFileHeader 4 | { 5 | public const string DefaultMinimumVisualStudioVersion = "10.0.40219.1"; 6 | 7 | public const string SupportedFileFormatVersion = "12.00"; 8 | 9 | /// 10 | /// Gets or sets the file format version. 11 | /// 12 | public string FileFormatVersion { get; init; } 13 | 14 | /// 15 | /// Gets or sets the last Visual Studio major version. 16 | /// 17 | public int? LastVisualStudioMajorVersion { get; init; } 18 | 19 | /// 20 | /// Gets or sets the last Visual Studio version. 21 | /// 22 | public Version? LastVisualStudioVersion { get; init; } 23 | 24 | /// 25 | /// Gets or sets the minimum Visual Studio version. 26 | /// 27 | public Version? MinimumVisualStudioVersion { get; init; } 28 | 29 | /// 30 | /// Initializes a new instance of the class. 31 | /// 32 | /// The file format version. 33 | /// The last Visual Studio major version. 34 | /// The last Visual Studio version. 35 | /// The minimum Visual Studio version. 36 | /// Only file format version {SupportedFileFormatVersion} is supported. - fileFormatVersion 37 | public SolutionFileHeader( 38 | string fileFormatVersion, 39 | int? lastVisualStudioMajorVersion = null, 40 | Version? lastVisualStudioVersion = null, 41 | Version? minimumVisualStudioVersion = null) 42 | { 43 | if (fileFormatVersion != SupportedFileFormatVersion) 44 | { 45 | throw new ArgumentException($"Only file format version {SupportedFileFormatVersion} is supported.", nameof(fileFormatVersion)); 46 | } 47 | 48 | this.FileFormatVersion = fileFormatVersion; 49 | this.LastVisualStudioMajorVersion = lastVisualStudioMajorVersion; 50 | this.LastVisualStudioVersion = lastVisualStudioVersion; 51 | this.MinimumVisualStudioVersion = minimumVisualStudioVersion; 52 | } 53 | 54 | /// 55 | /// Duplicates this instance and updates with the specified version information. 56 | /// 57 | /// The new visual studio version. 58 | /// . 59 | public SolutionFileHeader DuplicateAndUpdate(Version newVisualStudioVersion) => this with 60 | { 61 | LastVisualStudioMajorVersion = newVisualStudioVersion.Major, 62 | LastVisualStudioVersion = newVisualStudioVersion, 63 | }; 64 | } 65 | -------------------------------------------------------------------------------- /src/VisualStudio.VersionScraper/OutputFormat.cs: -------------------------------------------------------------------------------- 1 | namespace VisualStudio.VersionScraper; 2 | 3 | internal enum OutputFormat 4 | { 5 | Json, 6 | 7 | CSharp, 8 | } 9 | -------------------------------------------------------------------------------- /src/VisualStudio.VersionScraper/Program.cs: -------------------------------------------------------------------------------- 1 | namespace VisualStudio.VersionScraper; 2 | 3 | using System.CommandLine; 4 | using System.IO.Abstractions; 5 | 6 | using SlnUp.Core; 7 | using SlnUp.Json; 8 | 9 | using VisualStudio.VersionScraper.Writers.CSharp; 10 | 11 | internal static class Program 12 | { 13 | public static int Main(string[] args) 14 | { 15 | Console.Title = "Visual Studio Version Scraper"; 16 | 17 | return ProgramOptions.Configure(Run).Invoke(args); 18 | } 19 | 20 | private static int Run(ProgramOptions options) 21 | { 22 | if (string.IsNullOrWhiteSpace(options.OutputFilePath)) 23 | { 24 | Console.WriteLine("Invalid file path."); 25 | return 1; 26 | } 27 | 28 | VisualStudioVersionDocScraper docScraper = new(useCache: !options.NoCache); 29 | 30 | IEnumerable versions = docScraper.ScrapeVisualStudioVersions() 31 | .Where(v => !v.IsPreview) 32 | .OrderByDescending(x => x.BuildVersion); 33 | 34 | IFileSystem fileSystem = new FileSystem(); 35 | 36 | switch (options.Format) 37 | { 38 | case OutputFormat.Json: 39 | VisualStudioVersionJsonHelper.ToJsonFile(fileSystem, versions, options.OutputFilePath); 40 | break; 41 | 42 | case OutputFormat.CSharp: 43 | CSharpVersionWriter csharpWriter = new(fileSystem); 44 | csharpWriter.WriteClassToFile(versions, options.OutputFilePath); 45 | break; 46 | 47 | default: 48 | throw new NotSupportedException($"The format is not supported: '{options.Format}'."); 49 | } 50 | 51 | using (ConsoleHelpers.WithForegroundColor(ConsoleColor.Green)) 52 | { 53 | Console.WriteLine($"Done writing {options.Format} to '{options.OutputFilePath}'."); 54 | } 55 | 56 | return 0; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/VisualStudio.VersionScraper/ProgramOptions.cs: -------------------------------------------------------------------------------- 1 | namespace VisualStudio.VersionScraper; 2 | 3 | using System.CommandLine; 4 | using System.Diagnostics.CodeAnalysis; 5 | 6 | [SuppressMessage("Performance", "CA1812:Avoid uninstantiated internal classes", Justification = "Created at runtime.")] 7 | internal sealed class ProgramOptions 8 | { 9 | public OutputFormat Format { get; set; } 10 | 11 | public bool NoCache { get; set; } 12 | 13 | public string? OutputFilePath { get; set; } 14 | 15 | public static RootCommand Configure(Func invokeAction) 16 | { 17 | Argument outputArgument = new( 18 | name: "output", 19 | description: "The output file path."); 20 | 21 | Option formatOption = new( 22 | name: "--format", 23 | description: "The output file format."); 24 | formatOption.AddAlias("-f"); 25 | 26 | Option noCacheOption = new( 27 | name: "--no-cache", 28 | description: "Skip the cache when making requests."); 29 | 30 | RootCommand rootCommand = new("Visual Studio Version Scraper") 31 | { 32 | outputArgument, 33 | formatOption, 34 | noCacheOption, 35 | }; 36 | 37 | rootCommand.SetHandler(options => 38 | { 39 | return Task.FromResult(invokeAction(options)); 40 | }, new ProgramOptionsBinder(outputArgument, formatOption, noCacheOption)); 41 | 42 | return rootCommand; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/VisualStudio.VersionScraper/ProgramOptionsBinder.cs: -------------------------------------------------------------------------------- 1 | namespace VisualStudio.VersionScraper; 2 | 3 | using System.CommandLine; 4 | using System.CommandLine.Binding; 5 | 6 | internal sealed class ProgramOptionsBinder : BinderBase 7 | { 8 | private readonly Option formatOption; 9 | 10 | private readonly Option noCacheOption; 11 | 12 | private readonly Argument outputArgument; 13 | 14 | public ProgramOptionsBinder( 15 | Argument outputArgument, 16 | Option formatOption, 17 | Option noCacheOption) 18 | { 19 | ArgumentNullException.ThrowIfNull(outputArgument); 20 | ArgumentNullException.ThrowIfNull(formatOption); 21 | ArgumentNullException.ThrowIfNull(noCacheOption); 22 | 23 | this.outputArgument = outputArgument; 24 | this.formatOption = formatOption; 25 | this.noCacheOption = noCacheOption; 26 | } 27 | 28 | protected override ProgramOptions GetBoundValue(BindingContext bindingContext) => new() 29 | { 30 | OutputFilePath = bindingContext.ParseResult.GetValueForArgument(this.outputArgument), 31 | Format = bindingContext.ParseResult.GetValueForOption(this.formatOption), 32 | NoCache = bindingContext.ParseResult.GetValueForOption(this.noCacheOption), 33 | }; 34 | } 35 | -------------------------------------------------------------------------------- /src/VisualStudio.VersionScraper/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "VisualStudioVersionScraper": { 4 | "commandName": "Project" 5 | }, 6 | "Generate": { 7 | "commandName": "Project", 8 | "commandLineArgs": "../../../../../../src/SlnUp/Versions.json" 9 | }, 10 | "Generate (no-cache)": { 11 | "commandName": "Project", 12 | "commandLineArgs": "../../../../../../src/SlnUp/Versions.json --no-cache" 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /src/VisualStudio.VersionScraper/VisualStudio.VersionScraper.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net9.0 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/VisualStudio.VersionScraper/VisualStudioVersionDocScraper.cs: -------------------------------------------------------------------------------- 1 | namespace VisualStudio.VersionScraper; 2 | 3 | using System.Diagnostics.CodeAnalysis; 4 | using System.Globalization; 5 | using System.Text.RegularExpressions; 6 | 7 | using HtmlAgilityPack; 8 | 9 | using SlnUp.Core; 10 | using SlnUp.Core.Extensions; 11 | 12 | internal sealed class VisualStudioVersionDocScraper 13 | { 14 | private const string buildNumberColumnName = "Build Number"; 15 | 16 | private const string buildVersionColumnName = "Build version"; 17 | 18 | private const string channelColumnName = "Channel"; 19 | 20 | private const string releaseDateColumnName = "Release Date"; 21 | 22 | private const string versionColumnName = "Version"; 23 | 24 | private const string vs2017VersionsDocUrl = "https://docs.microsoft.com/en-us/previous-versions/visualstudio/visual-studio-2017/install/visual-studio-build-numbers-and-release-dates"; 25 | 26 | private const string vs2019VersionsDocUrl = "https://learn.microsoft.com/en-us/visualstudio/releases/2019/history"; 27 | 28 | private const string vsCurrentVersionsDocUrl = "https://docs.microsoft.com/en-us/visualstudio/install/visual-studio-build-numbers-and-release-dates"; 29 | 30 | private static readonly Regex vs15PreviewVersionMatcher = new( 31 | @"(?\d+\.\d+\.?\d*) (?Preview \d\.?\d*)", 32 | RegexOptions.Compiled); 33 | 34 | private readonly bool useCache; 35 | 36 | /// 37 | /// Initializes a new instance of the class. 38 | /// 39 | /// if set to true, use a cache. 40 | public VisualStudioVersionDocScraper(bool useCache = true) 41 | => this.useCache = useCache; 42 | 43 | /// 44 | /// Scrapes the visual studio versions from the documentation. 45 | /// 46 | /// . 47 | public IEnumerable ScrapeVisualStudioVersions() 48 | { 49 | HashSet versions = 50 | [ 51 | .. this.ScrapeVisualStudioVersions(vsCurrentVersionsDocUrl, "VSCurrentVersionCache"), 52 | .. this.ScrapeVisualStudioVersions(vs2019VersionsDocUrl, "VS2019VersionCache"), 53 | .. this.ScrapeVisualStudioVersions(vs2017VersionsDocUrl, "VS2017VersionCache"), 54 | ]; 55 | return versions; 56 | } 57 | 58 | private static VisualStudioVersion GetVersionDetailFromRow(RowData row) 59 | { 60 | string versionInput = row.Version; 61 | string channel = row.Channel ?? string.Empty; 62 | 63 | if (vs15PreviewVersionMatcher.TryMatch(versionInput, out Match? match) 64 | && Version.TryParse(match.Groups["version"].Value, out Version? version)) 65 | { 66 | if (version.Build == -1) 67 | { 68 | // Ensure a 3-part build number. 69 | version = new Version(version.Major, version.Minor, 0); 70 | } 71 | 72 | // The VS 2017 version and channel can be specified differently for preview versions. 73 | channel = match.Groups["channel"].Value; 74 | } 75 | else if (!Version.TryParse(versionInput, out version)) 76 | { 77 | throw new InvalidDataException($"First column did not contain a valid version value: '{versionInput}'."); 78 | } 79 | 80 | if (!Version.TryParse(row.BuildNumber, out Version? buildVersion)) 81 | { 82 | throw new InvalidDataException($"Third column did not contain a valid build version value: '{row.BuildNumber}'."); 83 | } 84 | 85 | VisualStudioProduct vsVersion = GetVisualStudioVersion(version); 86 | bool isPreview = channel.StartsWith("Preview", StringComparison.OrdinalIgnoreCase); 87 | 88 | return new VisualStudioVersion(vsVersion, version, buildVersion, channel, isPreview); 89 | } 90 | 91 | private static VisualStudioProduct GetVisualStudioVersion(Version version) 92 | => GetVisualStudioVersion(version.Major); 93 | 94 | private static VisualStudioProduct GetVisualStudioVersion(int majorVersion) => majorVersion switch 95 | { 96 | 15 => VisualStudioProduct.VisualStudio2017, 97 | 16 => VisualStudioProduct.VisualStudio2019, 98 | 17 => VisualStudioProduct.VisualStudio2022, 99 | _ => VisualStudioProduct.Unknown 100 | }; 101 | 102 | private static bool TryGetColumnIndices(HtmlNode table, out int versionIndex, out int releaseDateIndex, out int buildNumberIndex, out int? channelIndex) 103 | { 104 | const int notSet = -1; 105 | versionIndex = notSet; 106 | releaseDateIndex = notSet; 107 | buildNumberIndex = notSet; 108 | channelIndex = null; 109 | 110 | HtmlNodeCollection headings = table.SelectNodes("thead//th") 111 | ?? throw new InvalidOperationException("Unable to locate table headings."); 112 | 113 | string[] columnNames = [.. headings.Select(x => x.InnerText.Trim())]; 114 | 115 | for (int i = 0; i < columnNames.Length; i++) 116 | { 117 | string columnName = columnNames[i]; 118 | 119 | if (versionIndex is notSet 120 | && columnName.Equals(versionColumnName, StringComparison.OrdinalIgnoreCase)) 121 | { 122 | versionIndex = i; 123 | } 124 | else if (releaseDateIndex is notSet 125 | && columnName.Equals(releaseDateColumnName, StringComparison.OrdinalIgnoreCase)) 126 | { 127 | releaseDateIndex = i; 128 | } 129 | else if (buildNumberIndex is notSet 130 | && (columnName.Equals(buildNumberColumnName, StringComparison.OrdinalIgnoreCase) 131 | || columnName.Equals(buildVersionColumnName, StringComparison.OrdinalIgnoreCase))) 132 | { 133 | buildNumberIndex = i; 134 | } 135 | else if (!channelIndex.HasValue 136 | && columnName.Equals(channelColumnName, StringComparison.OrdinalIgnoreCase)) 137 | { 138 | channelIndex = i; 139 | } 140 | } 141 | 142 | return versionIndex is not notSet 143 | && releaseDateIndex is not notSet 144 | && buildNumberIndex is not notSet; 145 | } 146 | 147 | private static bool TryGetTableData( 148 | HtmlNode table, 149 | [NotNullWhen(true)] 150 | out IReadOnlyCollection? result) 151 | { 152 | result = null; 153 | 154 | if (table is null) 155 | { 156 | return false; 157 | } 158 | 159 | if (!TryGetColumnIndices(table, out int versionIndex, out int releaseDateIndex, out int buildNumberIndex, out int? channelIndex)) 160 | { 161 | return false; 162 | } 163 | 164 | HtmlNodeCollection? rows = table.SelectNodes("tbody/tr"); 165 | 166 | if (rows is null || rows.Count is 0) 167 | { 168 | return false; 169 | } 170 | 171 | List data = []; 172 | 173 | foreach (HtmlNode row in rows) 174 | { 175 | HtmlNodeCollection tds = row.SelectNodes("td") 176 | ?? throw new InvalidOperationException("Unable to locate table data."); 177 | 178 | string version = tds[versionIndex].InnerText.Trim(); 179 | string releaseDate = tds[releaseDateIndex].InnerText.Trim(); 180 | string buildNumber = tds[buildNumberIndex].InnerText.Trim(); 181 | string? channel = channelIndex.HasValue 182 | ? tds[channelIndex.Value].InnerText.Trim() 183 | : null; 184 | 185 | data.Add(new RowData(version, releaseDate, buildNumber, channel)); 186 | } 187 | 188 | result = data; 189 | return true; 190 | } 191 | 192 | private HtmlDocument LoadVisualStudioVersionDocument(string url, string cacheFolderName) 193 | { 194 | #pragma warning disable IO0006 // Replace Path class with IFileSystem.Path for improved testability 195 | string cachePath = Path.Join(Path.GetTempPath(), cacheFolderName); 196 | #pragma warning restore IO0006 // Replace Path class with IFileSystem.Path for improved testability 197 | HtmlWeb web = new() 198 | { 199 | CachePath = cachePath, 200 | UsingCache = this.useCache, 201 | }; 202 | 203 | UriBuilder uriBuilder = new(url); 204 | if (this.useCache) 205 | { 206 | // Add a cache query parameter. 207 | uriBuilder.Query = $"?cache={DateTime.Now.Date.ToString("MM-dd-yyyy", CultureInfo.CurrentCulture)}"; 208 | } 209 | 210 | HtmlDocument doc = web.Load(uriBuilder.Uri); 211 | 212 | return doc; 213 | } 214 | 215 | private IEnumerable ScrapeVisualStudioVersions(string url, string cacheFolderName) 216 | { 217 | HtmlDocument doc = this.LoadVisualStudioVersionDocument(url, cacheFolderName); 218 | 219 | HtmlNodeCollection tables = doc.DocumentNode.SelectNodes("//table") 220 | ?? throw new InvalidOperationException("Unable to locate any tables."); 221 | 222 | if (tables.Count == 0) 223 | { 224 | throw new InvalidDataException("No tables were found."); 225 | } 226 | 227 | IReadOnlyCollection? tableData = null; 228 | 229 | if (tables.Count > 1) 230 | { 231 | // Too many tables were found. We need to see if we can determine which one. 232 | foreach (HtmlNode table in tables) 233 | { 234 | if (TryGetTableData(table, out tableData)) 235 | { 236 | break; 237 | } 238 | } 239 | } 240 | else 241 | { 242 | TryGetTableData(tables.Single(), out tableData); 243 | } 244 | 245 | if (tableData is null) 246 | { 247 | throw new InvalidDataException("Unable to locate a suitable table or unable to retrieve table data."); 248 | } 249 | 250 | foreach (RowData rowData in tableData) 251 | { 252 | yield return GetVersionDetailFromRow(rowData); 253 | } 254 | } 255 | 256 | private sealed record RowData(string Version, string ReleaseDate, string BuildNumber, string? Channel); 257 | } 258 | -------------------------------------------------------------------------------- /src/VisualStudio.VersionScraper/Writers/CSharp/CSharpVersionWriter.cs: -------------------------------------------------------------------------------- 1 | namespace VisualStudio.VersionScraper.Writers.CSharp; 2 | 3 | using System.IO.Abstractions; 4 | 5 | using SlnUp.Core; 6 | 7 | internal sealed class CSharpVersionWriter 8 | { 9 | private const string ClassName = "VersionManager"; 10 | 11 | private const string GetVersionsMethodName = "GetDefaultVersions"; 12 | 13 | private const string NamespaceName = "SlnUp.Core"; 14 | 15 | private readonly IFileSystem fileSystem; 16 | 17 | public CSharpVersionWriter(IFileSystem fileSystem) 18 | { 19 | ArgumentNullException.ThrowIfNull(fileSystem); 20 | 21 | this.fileSystem = fileSystem; 22 | } 23 | 24 | public void WriteClassToFile(IEnumerable versions, string filePath) 25 | { 26 | using StreamWriter streamWriter = this.fileSystem.File.CreateText(filePath); 27 | CodeWriter writer = new(streamWriter); 28 | WriteHeader(writer); 29 | writer.WriteLine(); 30 | WriteClass(writer, versions); 31 | } 32 | 33 | private static void WriteClass(CodeWriter writer, IEnumerable versions) 34 | { 35 | writer.WriteLine($"namespace {NamespaceName};"); 36 | writer.WriteLine(); 37 | writer.WriteLine($"[System.CodeDom.Compiler.GeneratedCodeAttribute(\"{ThisAssembly.AssemblyName}\", \"{ThisAssembly.AssemblyVersion}\")]"); 38 | writer.WriteLine($"public partial class {ClassName}"); 39 | using (writer.WithBrackets()) 40 | { 41 | WriteLatestVersions(writer); 42 | writer.WriteLine(); 43 | WriteGetVersionsMethod(writer, versions); 44 | } 45 | } 46 | 47 | private static void WriteGetVersionsMethod(CodeWriter writer, IEnumerable versions) 48 | { 49 | writer.WriteLines(@" 50 | /// 51 | /// Gets the default versions. 52 | /// 53 | /// ."); 54 | writer.WriteLine($"public static IReadOnlyList<{nameof(VisualStudioVersion)}> {GetVersionsMethodName}()"); 55 | using (writer.WithBrackets()) 56 | { 57 | writer.WriteLine("return new []"); 58 | 59 | using (writer.WithBrackets(closingSemicolon: true)) 60 | { 61 | foreach (VisualStudioVersion version in versions) 62 | { 63 | string isPreview = version.IsPreview ? "true" : "false"; 64 | writer.WriteLine($"new {nameof(VisualStudioVersion)}({nameof(VisualStudioProduct)}.{version.Product}, Version.Parse(\"{version.Version}\"), Version.Parse(\"{version.BuildVersion}\"), \"{version.Channel}\", {isPreview}),"); 65 | } 66 | } 67 | } 68 | } 69 | 70 | private static void WriteHeader(CodeWriter writer) 71 | { 72 | writer.WriteLines($@" 73 | //------------------------------------------------------------------------------ 74 | // 75 | // This code was generated by a tool on {ThisAssembly.GitCommitDate:d}. 76 | // 77 | // Changes to this file may cause incorrect behavior and will be lost if 78 | // the code is regenerated. 79 | // 80 | //------------------------------------------------------------------------------"); 81 | } 82 | 83 | private static void WriteLatestVersions(CodeWriter writer) 84 | { 85 | VisualStudioProduct latestProduct = Enum.GetValues().Max(); 86 | writer.WriteLines(@" 87 | /// 88 | /// The latest product. 89 | /// "); 90 | writer.WriteLine($"public const {nameof(VisualStudioProduct)} LatestProduct = {nameof(VisualStudioProduct)}.{latestProduct};"); 91 | writer.WriteLine(); 92 | writer.WriteLines(@" 93 | /// 94 | /// The latest product value. 95 | /// "); 96 | writer.WriteLine($"public const string LatestProductValue = \"{(int)latestProduct}\";"); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/VisualStudio.VersionScraper/Writers/CodeWriter.cs: -------------------------------------------------------------------------------- 1 | namespace VisualStudio.VersionScraper.Writers; 2 | 3 | using System; 4 | using System.Diagnostics.CodeAnalysis; 5 | 6 | using SlnUp.Core; 7 | 8 | internal sealed class CodeWriter 9 | { 10 | private readonly int indentSpaces = 4; 11 | 12 | private readonly StreamWriter writer; 13 | 14 | private int currentIndentationLevel; 15 | 16 | /// 17 | /// Initializes a new instance of the class. 18 | /// 19 | /// The writer. 20 | public CodeWriter(StreamWriter writer) 21 | { 22 | ArgumentNullException.ThrowIfNull(writer); 23 | 24 | this.writer = writer; 25 | } 26 | 27 | /// 28 | /// Indents the code. 29 | /// 30 | public void Indent() => ++this.currentIndentationLevel; 31 | 32 | /// 33 | /// Unindents the code. 34 | /// 35 | public void Unindent() => --this.currentIndentationLevel; 36 | 37 | /// 38 | /// Creates a scope with brackets. 39 | /// 40 | /// if set to true, the closing bracket will include a semicolon. 41 | /// . 42 | public IDisposable WithBrackets(bool closingSemicolon = false) 43 | { 44 | this.WriteLine("{"); 45 | this.Indent(); 46 | return new ScopedAction(() => 47 | { 48 | this.Unindent(); 49 | 50 | if (closingSemicolon) 51 | { 52 | this.WriteLine("};"); 53 | } 54 | else 55 | { 56 | this.WriteLine("}"); 57 | } 58 | }); 59 | } 60 | 61 | /// 62 | /// Creates a scope with indentation. 63 | /// 64 | /// . 65 | public IDisposable WithIndent() 66 | { 67 | this.Indent(); 68 | return new ScopedAction(this.Unindent); 69 | } 70 | 71 | /// 72 | /// Writes the line. 73 | /// 74 | /// The value. 75 | /// . 76 | [SuppressMessage("Globalization", "CA1307:Specify StringComparison for clarity")] 77 | public CodeWriter WriteLine(string? value = null) 78 | { 79 | if (value is not null) 80 | { 81 | if (value.Contains(Environment.NewLine)) 82 | { 83 | throw new InvalidOperationException("Use WriteLines to write content that contains multiple lines."); 84 | } 85 | 86 | this.WriteIndentation(); 87 | } 88 | 89 | this.writer.WriteLine(value); 90 | 91 | return this; 92 | } 93 | 94 | /// 95 | /// Writes the lines. 96 | /// 97 | /// The value. 98 | /// . 99 | public CodeWriter WriteLines(string value) 100 | { 101 | foreach (string line in value.Split(Environment.NewLine, StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries)) 102 | { 103 | this.WriteLine(line); 104 | } 105 | 106 | return this; 107 | } 108 | 109 | private string GetIndentationWhiteSpace() => new(' ', this.indentSpaces * this.currentIndentationLevel); 110 | 111 | private void WriteIndentation() => this.writer.Write(this.GetIndentationWhiteSpace()); 112 | } 113 | -------------------------------------------------------------------------------- /tests/.editorconfig: -------------------------------------------------------------------------------- 1 | root = false 2 | 3 | # Project specific settings 4 | 5 | [*.cs] 6 | 7 | # CS1591: Missing XML comment for publicly visible type or member 8 | dotnet_diagnostic.CS1591.severity = none 9 | 10 | # CA1515: Consider making public types internal 11 | dotnet_diagnostic.CA1515.severity = none 12 | 13 | # CA1707: Identifiers should not contain underscores 14 | dotnet_diagnostic.CA1707.severity = none 15 | -------------------------------------------------------------------------------- /tests/Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | true 5 | 6 | 7 | false 8 | 9 | 10 | false 11 | 12 | 13 | true 14 | 15 | 16 | GeneratedCodeAttribute,CompilerGeneratedAttribute 17 | cobertura 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /tests/SlnUp.Core.Tests/Extensions/VersionExtensionsTests.cs: -------------------------------------------------------------------------------- 1 | namespace SlnUp.Core.Tests.Extensions; 2 | 3 | using SlnUp.Core.Extensions; 4 | 5 | public class VersionExtensionsTests 6 | { 7 | [Theory] 8 | [InlineData("0.0", "0.0", true)] 9 | [InlineData("0.0", "0.1", false)] 10 | [InlineData("0.0.0", "0.0.0", true)] 11 | [InlineData("0.0.0", "0.1.0", false)] 12 | [InlineData("0.0.0", "0.0.1", true)] 13 | [InlineData("0.0.0.0", "0.0.0.0", true)] 14 | [InlineData("0.0.0.0", "0.1.0.0", false)] 15 | [InlineData("0.0.0.0", "0.0.0.1", true)] 16 | public void HasSameMajorMinor(string versionAInput, string versionBInput, bool expectedResult) 17 | { 18 | // Arrange 19 | Version versionA = Version.Parse(versionAInput); 20 | Version versionB = Version.Parse(versionBInput); 21 | 22 | // Act 23 | bool result = versionA.HasSameMajorMinor(versionB); 24 | 25 | // Assert 26 | Assert.Equal(expectedResult, result); 27 | } 28 | 29 | [Theory] 30 | [InlineData("0.0", true)] 31 | [InlineData("0.0.0", false)] 32 | [InlineData("0.0.0.0", false)] 33 | public void Is2PartVersion(string versionInput, bool expectedResult) 34 | { 35 | // Arrange 36 | Version version = Version.Parse(versionInput); 37 | 38 | // Act 39 | bool result = version.Is2PartVersion(); 40 | 41 | // Assert 42 | Assert.Equal(expectedResult, result); 43 | } 44 | 45 | [Theory] 46 | [InlineData("0.0", false)] 47 | [InlineData("0.0.0", true)] 48 | [InlineData("0.0.0.0", false)] 49 | public void Is3PartVersion(string versionInput, bool expectedResult) 50 | { 51 | // Arrange 52 | Version version = Version.Parse(versionInput); 53 | 54 | // Act 55 | bool result = version.Is3PartVersion(); 56 | 57 | // Assert 58 | Assert.Equal(expectedResult, result); 59 | } 60 | 61 | [Theory] 62 | [InlineData("0.0", false)] 63 | [InlineData("0.0.0", false)] 64 | [InlineData("0.0.0.0", true)] 65 | public void Is4PartVersion(string versionInput, bool expectedResult) 66 | { 67 | // Arrange 68 | Version version = Version.Parse(versionInput); 69 | 70 | // Act 71 | bool result = version.Is4PartVersion(); 72 | 73 | // Assert 74 | Assert.Equal(expectedResult, result); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /tests/SlnUp.Core.Tests/ScopedActionTests.cs: -------------------------------------------------------------------------------- 1 | namespace SlnUp.Core.Tests; 2 | 3 | public class ScopedActionTests 4 | { 5 | [Fact] 6 | public void ScopedAction_Invoked() 7 | { 8 | // Arrange 9 | int actionCalled = 0; 10 | ScopedAction action = new(() => ++actionCalled); 11 | 12 | // Act 13 | action.Dispose(); 14 | action.Dispose(); 15 | 16 | // Assert 17 | Assert.Equal(1, actionCalled); 18 | } 19 | 20 | [Fact] 21 | public void ScopedAction_InvokedFromUsing() 22 | { 23 | // Arrange 24 | bool actionCalled = false; 25 | 26 | // Act 27 | { 28 | using ScopedAction action = new(() => actionCalled = true); 29 | } 30 | 31 | // Assert 32 | Assert.True(actionCalled); 33 | } 34 | 35 | [Fact] 36 | public void ScopedAction_Null() 37 | { 38 | // Arrange 39 | ScopedAction action = new(null!); 40 | 41 | // Act and assert 42 | action.Dispose(); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /tests/SlnUp.Core.Tests/SlnUp.Core.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net9.0;net8.0 5 | Exe 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | %(Identity) 16 | 17 | 18 | 19 | 20 | 21 | all 22 | runtime; build; native; contentfiles; analyzers; buildtransitive 23 | 24 | 25 | all 26 | runtime; build; native; contentfiles; analyzers 27 | 28 | 29 | 30 | 31 | 32 | 33 | all 34 | runtime; build; native; contentfiles; analyzers; buildtransitive 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /tests/SlnUp.Core.Tests/VersionManagerTests.cs: -------------------------------------------------------------------------------- 1 | namespace SlnUp.Core.Tests; 2 | 3 | using System.Diagnostics.CodeAnalysis; 4 | 5 | using SlnUp.Json; 6 | 7 | public class VersionManagerTests 8 | { 9 | [Fact] 10 | [SuppressMessage("Globalization", "CA1305:Specify IFormatProvider")] 11 | public void Construct() 12 | { 13 | // Act 14 | VersionManager versionManager = new(); 15 | 16 | // Assert 17 | foreach (VisualStudioProduct product in Enum.GetValues()) 18 | { 19 | if (product is VisualStudioProduct.Unknown) 20 | { 21 | continue; 22 | } 23 | 24 | VisualStudioVersion? version = versionManager.FromVersionParameter(((int)product).ToString()); 25 | Assert.NotNull(version); 26 | } 27 | } 28 | 29 | [Fact] 30 | [SuppressMessage("Globalization", "CA1305:Specify IFormatProvider")] 31 | public void Construct_Versions() 32 | { 33 | // Arrange 34 | IReadOnlyList versions = 35 | [ 36 | new VisualStudioVersion(VisualStudioProduct.VisualStudio2022, Version.Parse("17.2.5"), Version.Parse("17.2.32616.157"), "Release", false), 37 | ]; 38 | 39 | // Act 40 | VersionManager versionManager = new(versions); 41 | 42 | // Assert 43 | Assert.NotNull(versionManager.FromVersionParameter(((int)VisualStudioProduct.VisualStudio2022).ToString())); 44 | Assert.Null(versionManager.FromVersionParameter(((int)VisualStudioProduct.VisualStudio2017).ToString())); 45 | } 46 | 47 | [Theory] 48 | [InlineData(null, false, null)] 49 | [InlineData("", false, null)] 50 | [InlineData(" ", false, null)] 51 | [InlineData("0", false, null)] 52 | [InlineData("0.0", false, null)] 53 | [InlineData("0.0.0", false, null)] 54 | [InlineData("0.0.0.0", false, null)] 55 | [InlineData("15.2", true, "15.0.26430.16")] 56 | [InlineData("15.2.5", true, "15.0.26430.15")] 57 | [InlineData("15.99", false, null)] 58 | [InlineData("15.2.99", false, null)] 59 | [InlineData("2017", true, "15.9.28307.1778")] 60 | [InlineData("16.9", true, "16.9.32106.192")] 61 | [InlineData("16.7.21", true, "16.7.31828.227")] 62 | [InlineData("16.99", false, null)] 63 | [InlineData("16.7.99", false, null)] 64 | [InlineData("2019", true, "16.11.32106.194")] 65 | [InlineData("17.0", true, "17.0.32112.339")] 66 | [InlineData("17.0.0", true, "17.0.31903.59")] 67 | [InlineData("17.99", false, null)] 68 | [InlineData("17.0.99", false, null)] 69 | [InlineData("2022", true, "17.0.32112.339")] 70 | public void FromVersionParameter(string? input, bool expectFound, string? expectedBuildVersion) 71 | { 72 | // Arrange 73 | if (expectFound && expectedBuildVersion is null) 74 | { 75 | throw new ArgumentNullException(nameof(expectedBuildVersion), "We expect a value to be found but an expected build version wasn't provided."); 76 | } 77 | 78 | VersionManager versionManager = VersionManagerJsonHelper.LoadFromEmbeddedResource(typeof(VersionManagerTests).Assembly, "TestVersions.json"); 79 | 80 | // Act 81 | VisualStudioVersion? version = versionManager.FromVersionParameter(input); 82 | 83 | // Assert 84 | if (!expectFound) 85 | { 86 | Assert.Null(version); 87 | } 88 | else 89 | { 90 | Assert.NotNull(version); 91 | Assert.Equal(Version.Parse(expectedBuildVersion!), version.BuildVersion); 92 | } 93 | } 94 | 95 | [Theory] 96 | [InlineData(null, false)] 97 | [InlineData("", false)] 98 | [InlineData(" ", false)] 99 | [InlineData("0", false)] 100 | [InlineData("0.0", true)] 101 | [InlineData("0.0.0", true)] 102 | [InlineData("0.0.0.0", false)] 103 | [InlineData("2022", false)] 104 | public void TryParseVisualStudioVersion(string? input, bool expectedResult) 105 | { 106 | // Act 107 | bool result = VersionManager.TryParseVisualStudioVersion(input, out Version? version); 108 | 109 | // Assert 110 | Assert.Equal(expectedResult, result); 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /tests/SlnUp.Core.Tests/VisualStudioVersionTests.cs: -------------------------------------------------------------------------------- 1 | namespace SlnUp.Core.Tests; 2 | 3 | public class VisualStudioVersionTests 4 | { 5 | private static readonly IReadOnlyList commonVersions = 6 | [ 7 | new VisualStudioVersion( 8 | VisualStudioProduct.VisualStudio2019, 9 | Version.Parse("16.11.6"), 10 | Version.Parse("16.11.31829.152"), 11 | "Release"), 12 | new VisualStudioVersion( 13 | VisualStudioProduct.VisualStudio2022, 14 | Version.Parse("17.0.0"), 15 | Version.Parse("17.0.31903.59"), 16 | "Release") 17 | ]; 18 | 19 | [Fact] 20 | public void Constructor() 21 | { 22 | // Arrange 23 | const string expectedChannel = "Release"; 24 | const string expectedFullVersionTitle = "Visual Studio 2022 17.0"; 25 | const string expectedVersionTitle = "Visual Studio 2022"; 26 | const string expectedVersion = "17.0.0"; 27 | const string expectedBuildVersion = "17.0.31903.59"; 28 | 29 | // Act 30 | VisualStudioVersion version = new( 31 | VisualStudioProduct.VisualStudio2022, 32 | Version.Parse(expectedVersion), 33 | Version.Parse(expectedBuildVersion), 34 | expectedChannel); 35 | 36 | // Assert 37 | Assert.Equal(expectedChannel, version.Channel); 38 | Assert.Equal(expectedVersion, version.Version.ToString()); 39 | Assert.Equal(expectedBuildVersion, version.BuildVersion.ToString()); 40 | Assert.Equal(expectedVersionTitle, version.ProductTitle); 41 | Assert.Equal(expectedFullVersionTitle, version.FullProductTitle); 42 | } 43 | 44 | [Fact] 45 | public void Constructor_WithPreview() 46 | { 47 | // Arrange 48 | const string expectedChannel = "Preview 1"; 49 | const string expectedFullVersionTitle = "Visual Studio 2022 17.0.1 Preview 1"; 50 | const string expectedVersionTitle = "Visual Studio 2022"; 51 | const string expectedVersion = "17.0.1"; 52 | const string expectedBuildVersion = "17.1.31903.286"; 53 | 54 | // Act 55 | VisualStudioVersion version = new( 56 | VisualStudioProduct.VisualStudio2022, 57 | Version.Parse(expectedVersion), 58 | Version.Parse(expectedBuildVersion), 59 | expectedChannel, 60 | isPreview: true); 61 | 62 | // Assert 63 | Assert.Equal(expectedChannel, version.Channel); 64 | Assert.Equal(expectedVersion, version.Version.ToString()); 65 | Assert.Equal(expectedBuildVersion, version.BuildVersion.ToString()); 66 | Assert.Equal(expectedVersionTitle, version.ProductTitle); 67 | Assert.Equal(expectedFullVersionTitle, version.FullProductTitle); 68 | } 69 | 70 | [Fact] 71 | public void Equals_NullParameter_ReturnsFalse() 72 | { 73 | // Arrange 74 | VisualStudioVersion version = commonVersions[0]; 75 | VisualStudioVersion? other = null; 76 | 77 | // Act 78 | bool result = version.Equals(other); 79 | 80 | // Assert 81 | Assert.False(result); 82 | } 83 | 84 | [Fact] 85 | public void Equals_SameReference_ReturnsTrue() 86 | { 87 | // Arrange 88 | VisualStudioVersion version = commonVersions[0]; 89 | VisualStudioVersion? other = commonVersions[0]; 90 | 91 | // Act 92 | bool result = version.Equals(other); 93 | 94 | // Assert 95 | Assert.True(result); 96 | } 97 | 98 | [Fact] 99 | public void Equals_SameValue_ReturnsTrue() 100 | { 101 | // Arrange 102 | VisualStudioVersion version = new( 103 | VisualStudioProduct.VisualStudio2022, 104 | Version.Parse("17.0.0"), 105 | Version.Parse("17.0.31903.59"), 106 | "Release"); 107 | VisualStudioVersion? other = new( 108 | VisualStudioProduct.VisualStudio2022, 109 | Version.Parse("17.0.0"), 110 | Version.Parse("17.0.31903.59"), 111 | "Release"); 112 | 113 | // Act 114 | bool result = version.Equals(other); 115 | 116 | // Assert 117 | Assert.True(result); 118 | } 119 | 120 | [Fact] 121 | public void Equals_WithObject_NullParameter_ReturnsFalse() 122 | { 123 | // Arrange 124 | VisualStudioVersion version = commonVersions[0]; 125 | object? other = null; 126 | 127 | // Act 128 | bool result = version.Equals(other); 129 | 130 | // Assert 131 | Assert.False(result); 132 | } 133 | 134 | [Fact] 135 | public void Equals_WithObject_SameValue_ReturnsTrue() 136 | { 137 | // Arrange 138 | VisualStudioVersion version = new( 139 | VisualStudioProduct.VisualStudio2022, 140 | Version.Parse("17.0.0"), 141 | Version.Parse("17.0.31903.59"), 142 | "Release"); 143 | object? other = new VisualStudioVersion( 144 | VisualStudioProduct.VisualStudio2022, 145 | Version.Parse("17.0.0"), 146 | Version.Parse("17.0.31903.59"), 147 | "Release"); 148 | 149 | // Act 150 | bool result = version.Equals(other); 151 | 152 | // Assert 153 | Assert.True(result); 154 | } 155 | 156 | [Fact] 157 | public void GetHashCodeMethod() 158 | { 159 | // Arrange 160 | VisualStudioVersion version = commonVersions[0]; 161 | int expectedHashCode = version.BuildVersion.GetHashCode(); 162 | 163 | // Act 164 | int hashCode = version.GetHashCode(); 165 | 166 | // Assert 167 | Assert.Equal(expectedHashCode, hashCode); 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /tests/SlnUp.Json.Tests/Extensions/AssemblyExtensionsTests.cs: -------------------------------------------------------------------------------- 1 | namespace SlnUp.Json.Tests.Extensions; 2 | 3 | using SlnUp.Json.Extensions; 4 | 5 | public class AssemblyExtensionsTests 6 | { 7 | [Fact] 8 | public void GetEmbeddedFileResourceContent() 9 | { 10 | // Arrange 11 | string resourceName = "TestVersions.json"; 12 | 13 | // Act 14 | string content = AssemblyExtensions.GetEmbeddedFileResourceContent( 15 | typeof(AssemblyExtensionsTests).Assembly, 16 | resourceName); 17 | 18 | // Assert 19 | Assert.NotNull(content); 20 | } 21 | 22 | [Fact] 23 | public void GetEmbeddedFileResourceContent_ResourceNotFound() 24 | { 25 | // Arrange, act, and assert 26 | Assert.Throws(() => AssemblyExtensions.GetEmbeddedFileResourceContent( 27 | typeof(AssemblyExtensionsTests).Assembly, 28 | "nothing")); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /tests/SlnUp.Json.Tests/SlnUp.Json.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net9.0;net8.0 5 | Exe 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | %(Filename)%(Extension) 16 | 17 | 18 | 19 | 20 | 21 | all 22 | runtime; build; native; contentfiles; analyzers; buildtransitive 23 | 24 | 25 | all 26 | runtime; build; native; contentfiles; analyzers 27 | 28 | 29 | 30 | 31 | 32 | 33 | all 34 | runtime; build; native; contentfiles; analyzers; buildtransitive 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /tests/SlnUp.Json.Tests/VersionManagerJsonHelperTests.cs: -------------------------------------------------------------------------------- 1 | namespace SlnUp.Json.Tests; 2 | 3 | using SlnUp.Core; 4 | 5 | public class VersionManagerJsonHelperTests 6 | { 7 | [Fact] 8 | public void LoadFromEmbeddedResource() 9 | { 10 | // Arrange 11 | string resourceName = "TestVersions.json"; 12 | 13 | // Act 14 | VersionManager versionManager = VersionManagerJsonHelper.LoadFromEmbeddedResource( 15 | typeof(VersionManagerJsonHelperTests).Assembly, 16 | resourceName); 17 | 18 | // Assert 19 | Assert.NotNull(versionManager); 20 | } 21 | 22 | [Fact] 23 | public void LoadFromEmbeddedResource_ResourceNotFound() 24 | { 25 | // Arrange, act, and assert 26 | Assert.Throws(() => VersionManagerJsonHelper.LoadFromEmbeddedResource( 27 | typeof(VersionManagerJsonHelperTests).Assembly, 28 | "nothing")); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /tests/SlnUp.Json.Tests/VisualStudioVersionJsonHelperTests.cs: -------------------------------------------------------------------------------- 1 | namespace SlnUp.Json.Tests; 2 | 3 | using System.IO.Abstractions; 4 | using System.IO.Abstractions.TestingHelpers; 5 | using System.Text.Json; 6 | 7 | using SlnUp.Core; 8 | using SlnUp.TestLibrary.Extensions; 9 | 10 | public class VisualStudioVersionJsonHelperTests 11 | { 12 | private const string commonVersionsJson = /*lang=json,strict*/ @"[ 13 | { 14 | ""BuildVersion"": ""16.11.31829.152"", 15 | ""Channel"": ""Release"", 16 | ""IsPreview"": false, 17 | ""Product"": ""visualStudio2019"", 18 | ""Version"": ""16.11.6"" 19 | }, 20 | { 21 | ""BuildVersion"": ""17.0.31903.59"", 22 | ""Channel"": ""Release"", 23 | ""IsPreview"": false, 24 | ""Product"": ""visualStudio2022"", 25 | ""Version"": ""17.0.0"" 26 | } 27 | ]"; 28 | 29 | private static readonly IReadOnlyList commonVersions = 30 | [ 31 | new VisualStudioVersion( 32 | VisualStudioProduct.VisualStudio2019, 33 | Version.Parse("16.11.6"), 34 | Version.Parse("16.11.31829.152"), 35 | "Release"), 36 | new VisualStudioVersion( 37 | VisualStudioProduct.VisualStudio2022, 38 | Version.Parse("17.0.0"), 39 | Version.Parse("17.0.31903.59"), 40 | "Release"), 41 | ]; 42 | 43 | [Fact] 44 | public void FromJson() 45 | { 46 | // Act 47 | IReadOnlyList versions = VisualStudioVersionJsonHelper.FromJson(commonVersionsJson); 48 | 49 | // Assert 50 | Assert.Equal(commonVersions, versions); 51 | } 52 | 53 | [Fact] 54 | public void FromJson_Empty() 55 | { 56 | // Act 57 | IReadOnlyList versions = VisualStudioVersionJsonHelper.FromJson("[]"); 58 | 59 | // Assert 60 | Assert.Empty(versions); 61 | } 62 | 63 | [Fact] 64 | public void FromJson_EmptyString() 65 | { 66 | // Act 67 | Assert.Throws(() => VisualStudioVersionJsonHelper.FromJson(string.Empty)); 68 | } 69 | 70 | [Fact] 71 | public void FromJson_Null() 72 | { 73 | // Act 74 | Assert.Throws(() => VisualStudioVersionJsonHelper.FromJson("null")); 75 | } 76 | 77 | [Fact] 78 | public void FromJsonFile() 79 | { 80 | // Arrange 81 | string filePath = "C:/foo.json".ToCrossPlatformPath(); 82 | IFileSystem fileSystem = new MockFileSystem(new Dictionary 83 | { 84 | [filePath] = new MockFileData(commonVersionsJson), 85 | }); 86 | 87 | // Act 88 | IReadOnlyList versions = VisualStudioVersionJsonHelper.FromJsonFile(fileSystem, filePath); 89 | 90 | // Assert 91 | Assert.Equal(commonVersions, versions); 92 | } 93 | 94 | [Fact] 95 | public void ToJson() 96 | { 97 | // Act 98 | string json = VisualStudioVersionJsonHelper.ToJson(commonVersions); 99 | 100 | // Assert 101 | Assert.Equal(commonVersionsJson, json); 102 | } 103 | 104 | [Fact] 105 | public void ToJson_Empty() 106 | { 107 | // Arrange 108 | const string expectedJson = "[]"; 109 | 110 | // Act 111 | string json = VisualStudioVersionJsonHelper.ToJson([]); 112 | 113 | // Assert 114 | Assert.Equal(expectedJson, json); 115 | } 116 | 117 | [Fact] 118 | public void ToJson_Null() 119 | { 120 | // Arrange 121 | const string expectedJson = "null"; 122 | 123 | // Act 124 | string json = VisualStudioVersionJsonHelper.ToJson(null!); 125 | 126 | // Assert 127 | Assert.Equal(expectedJson, json); 128 | } 129 | 130 | [Fact] 131 | public void ToJsonFile() 132 | { 133 | // Arrange 134 | MockFileSystem fileSystem = new(); 135 | string filePath = "C:/foo.json".ToCrossPlatformPath(); 136 | 137 | // Act 138 | VisualStudioVersionJsonHelper.ToJsonFile(fileSystem, commonVersions, filePath); 139 | 140 | // Assert 141 | Assert.True(fileSystem.FileExists(filePath)); 142 | Assert.Equal(commonVersionsJson, fileSystem.GetFile(filePath).TextContents); 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /tests/SlnUp.TestLibrary/Extensions/StringExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace SlnUp.TestLibrary.Extensions; 2 | 3 | using System.IO.Abstractions.TestingHelpers; 4 | 5 | /// 6 | /// Class StringExtensions. 7 | /// 8 | public static class StringExtensions 9 | { 10 | /// 11 | /// Converts to a cross-platform path from a Windows path. 12 | /// 13 | /// The path. 14 | /// . 15 | public static string ToCrossPlatformPath(this string path) 16 | => MockUnixSupport.Path(path); 17 | 18 | /// 19 | /// Converts to cross-platform paths from Windows paths. 20 | /// 21 | /// The paths. 22 | /// . 23 | public static IEnumerable ToCrossPlatformPath(this IEnumerable paths) 24 | => paths.Select(MockUnixSupport.Path); 25 | } 26 | -------------------------------------------------------------------------------- /tests/SlnUp.TestLibrary/ScopedDirectory.cs: -------------------------------------------------------------------------------- 1 | namespace SlnUp.TestLibrary; 2 | 3 | using System.IO.Abstractions; 4 | 5 | using SlnUp.Core; 6 | 7 | /// 8 | /// Class ScopedDirectory. 9 | /// Implements the 10 | /// 11 | /// 12 | public class ScopedDirectory : IDisposable 13 | { 14 | private readonly ScopedAction cleanupAction; 15 | 16 | private bool disposedValue; 17 | 18 | /// 19 | /// Gets the file system. 20 | /// 21 | public IFileSystem FileSystem { get; } 22 | 23 | /// 24 | /// Gets the directory path. 25 | /// 26 | public string Path { get; } 27 | 28 | /// 29 | /// Initializes a new instance of the class. 30 | /// 31 | /// The directory path. 32 | public ScopedDirectory(string path) 33 | : this(new FileSystem(), path) 34 | { 35 | } 36 | 37 | /// 38 | /// Initializes a new instance of the class. 39 | /// 40 | /// The file system. 41 | /// The directory path. 42 | public ScopedDirectory(IFileSystem fileSystem, string path) 43 | { 44 | ArgumentNullException.ThrowIfNull(fileSystem); 45 | ArgumentException.ThrowIfNullOrWhiteSpace(path); 46 | 47 | this.FileSystem = fileSystem; 48 | this.Path = path; 49 | 50 | if (!fileSystem.Directory.Exists(path)) 51 | { 52 | fileSystem.Directory.CreateDirectory(path); 53 | } 54 | 55 | this.cleanupAction = new ScopedAction(this.Cleanup); 56 | } 57 | 58 | /// 59 | /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. 60 | /// 61 | public void Dispose() 62 | { 63 | this.Dispose(disposing: true); 64 | GC.SuppressFinalize(this); 65 | } 66 | 67 | /// 68 | /// Gets the path to a temporary file with the specified file name. 69 | /// 70 | /// The name of the file. 71 | /// A path to a temporary file. 72 | public string GetPathWithFileName(string fileName) => this.FileSystem.Path.Combine(this.Path, fileName); 73 | 74 | /// 75 | /// Gets a path to a random temporary file. The file is not created. 76 | /// 77 | /// A path to a random temporary file. 78 | public string GetRandomFilePath() 79 | => this.GetPathWithFileName(TemporaryFile.GetRandomFileName()); 80 | 81 | /// 82 | /// Gets a path to a random temporary file. The file is not created. 83 | /// 84 | /// The file extension. No '.'. 85 | /// A path to a random temporary file. 86 | public string GetRandomFilePathWithExtension(string extension) 87 | => this.GetPathWithFileName(TemporaryFile.GetRandomFileNameWithExtension(extension)); 88 | 89 | /// 90 | /// Sets the directory as the working directory using scoped behavior. 91 | /// 92 | /// . 93 | public IDisposable SetAsScopedWorkingDirectory() 94 | => WorkingDirectory.SetScoped(this.FileSystem, this.Path); 95 | 96 | /// 97 | /// Releases unmanaged and - optionally - managed resources. 98 | /// 99 | /// 100 | /// true to release both managed and unmanaged resources; false to release only unmanaged resources. 101 | /// 102 | protected virtual void Dispose(bool disposing) 103 | { 104 | if (!this.disposedValue) 105 | { 106 | if (disposing) 107 | { 108 | this.cleanupAction.Dispose(); 109 | } 110 | 111 | this.disposedValue = true; 112 | } 113 | } 114 | 115 | private void Cleanup() 116 | { 117 | if (this.FileSystem.Directory.Exists(this.Path)) 118 | { 119 | this.FileSystem.Directory.Delete(this.Path, recursive: true); 120 | } 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /tests/SlnUp.TestLibrary/ScopedFile.cs: -------------------------------------------------------------------------------- 1 | namespace SlnUp.TestLibrary; 2 | 3 | using System.IO.Abstractions; 4 | 5 | using SlnUp.Core; 6 | 7 | /// 8 | /// Class ScopedFile. 9 | /// Implements the 10 | /// 11 | /// 12 | public class ScopedFile : IDisposable 13 | { 14 | private readonly ScopedAction cleanupAction; 15 | 16 | private bool disposedValue; 17 | 18 | /// 19 | /// Gets the file system. 20 | /// 21 | public IFileSystem FileSystem { get; } 22 | 23 | /// 24 | /// Gets the file path. 25 | /// 26 | public string Path { get; } 27 | 28 | /// 29 | /// Initializes a new instance of the class. 30 | /// 31 | /// The file path. 32 | public ScopedFile(string filePath) 33 | : this(new FileSystem(), filePath) 34 | { 35 | } 36 | 37 | /// 38 | /// Initializes a new instance of the class. 39 | /// 40 | /// The file system. 41 | /// The file path. 42 | public ScopedFile(IFileSystem fileSystem, string filePath) 43 | { 44 | ArgumentNullException.ThrowIfNull(fileSystem); 45 | ArgumentException.ThrowIfNullOrWhiteSpace(filePath); 46 | 47 | this.FileSystem = fileSystem; 48 | this.Path = filePath; 49 | 50 | if (!fileSystem.File.Exists(filePath)) 51 | { 52 | fileSystem.File.WriteAllText(this.Path, string.Empty); 53 | } 54 | 55 | this.cleanupAction = new ScopedAction(this.Cleanup); 56 | } 57 | 58 | /// 59 | /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. 60 | /// 61 | public void Dispose() 62 | { 63 | this.Dispose(disposing: true); 64 | GC.SuppressFinalize(this); 65 | } 66 | 67 | /// 68 | /// Releases unmanaged and - optionally - managed resources. 69 | /// 70 | /// 71 | /// true to release both managed and unmanaged resources; false to release only unmanaged resources. 72 | /// 73 | protected virtual void Dispose(bool disposing) 74 | { 75 | if (!this.disposedValue) 76 | { 77 | if (disposing) 78 | { 79 | this.cleanupAction.Dispose(); 80 | } 81 | 82 | this.disposedValue = true; 83 | } 84 | } 85 | 86 | private void Cleanup() 87 | { 88 | if (this.FileSystem.File.Exists(this.Path)) 89 | { 90 | this.FileSystem.File.Delete(this.Path); 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /tests/SlnUp.TestLibrary/SlnUp.TestLibrary.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net9.0;net8.0 5 | 6 | false 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /tests/SlnUp.TestLibrary/TemporaryDirectory.cs: -------------------------------------------------------------------------------- 1 | namespace SlnUp.TestLibrary; 2 | 3 | using System.IO.Abstractions; 4 | 5 | /// 6 | /// Class TemporaryDirectory. 7 | /// 8 | public static class TemporaryDirectory 9 | { 10 | /// 11 | /// Creates a temporary directory. 12 | /// 13 | /// The name of the directory. 14 | /// . 15 | public static ScopedDirectory Create(string directoryName) 16 | => Create(new FileSystem(), directoryName); 17 | 18 | /// 19 | /// Creates a temporary directory. 20 | /// 21 | /// The file system. 22 | /// The name of the directory. 23 | /// . 24 | public static ScopedDirectory Create(IFileSystem fileSystem, string directoryName) 25 | { 26 | ArgumentNullException.ThrowIfNull(fileSystem); 27 | 28 | string directoryPath = GetPathWithName(fileSystem, directoryName); 29 | 30 | return new(fileSystem, directoryPath); 31 | } 32 | 33 | /// 34 | /// Creates a random temporary directory. 35 | /// 36 | /// . 37 | public static ScopedDirectory CreateRandom() 38 | => CreateRandom(new FileSystem()); 39 | 40 | /// 41 | /// Creates a random temporary directory. 42 | /// 43 | /// The file system. 44 | /// . 45 | public static ScopedDirectory CreateRandom(IFileSystem fileSystem) 46 | => Create(fileSystem, GetRandomDirectoryName()); 47 | 48 | /// 49 | /// Gets the path to the temporary directory. 50 | /// 51 | /// The file system. 52 | /// . 53 | public static string GetPath(IFileSystem fileSystem) 54 | { 55 | ArgumentNullException.ThrowIfNull(fileSystem); 56 | 57 | return fileSystem.Path.GetTempPath(); 58 | } 59 | 60 | /// 61 | /// Gets the path to a temporary directory with the specified name. 62 | /// 63 | /// The file system. 64 | /// Name of the directory. 65 | /// . 66 | public static string GetPathWithName(IFileSystem fileSystem, string directoryName) 67 | { 68 | ArgumentNullException.ThrowIfNull(fileSystem); 69 | ArgumentException.ThrowIfNullOrWhiteSpace(directoryName); 70 | 71 | return fileSystem.Path.Combine(GetPath(fileSystem), directoryName); 72 | } 73 | 74 | /// 75 | /// Gets a random directory name. 76 | /// 77 | /// A random directory name. 78 | public static string GetRandomDirectoryName() => Guid.NewGuid().ToString(); 79 | } 80 | -------------------------------------------------------------------------------- /tests/SlnUp.TestLibrary/TemporaryFile.cs: -------------------------------------------------------------------------------- 1 | namespace SlnUp.TestLibrary; 2 | 3 | using System.IO.Abstractions; 4 | 5 | /// 6 | /// Class TemporaryFile. 7 | /// 8 | public static class TemporaryFile 9 | { 10 | /// 11 | /// Creates a temporary file. 12 | /// 13 | /// The name of the file. 14 | /// . 15 | public static ScopedFile Create(string fileName) 16 | => Create(new FileSystem(), fileName); 17 | 18 | /// 19 | /// Creates a temporary file. 20 | /// 21 | /// The file system. 22 | /// The name of the file. 23 | /// . 24 | public static ScopedFile Create(IFileSystem fileSystem, string fileName) 25 | { 26 | ArgumentNullException.ThrowIfNull(fileSystem); 27 | 28 | string filePath = GetPathWithFileName(fileSystem, fileName); 29 | return new(fileSystem, filePath); 30 | } 31 | 32 | /// 33 | /// Creates a random temporary file. 34 | /// 35 | /// . 36 | public static ScopedFile CreateRandom() 37 | => CreateRandom(new FileSystem()); 38 | 39 | /// 40 | /// Creates a random temporary file. 41 | /// 42 | /// The file system. 43 | /// . 44 | public static ScopedFile CreateRandom(IFileSystem fileSystem) 45 | => Create(fileSystem, GetRandomFileName()); 46 | 47 | /// 48 | /// Creates a random temporary file with the specified extension. 49 | /// 50 | /// The file extension. No '.'. 51 | /// . 52 | public static ScopedFile CreateRandomWithExtension(string extension) 53 | => Create(new FileSystem(), GetRandomFileNameWithExtension(extension)); 54 | 55 | /// 56 | /// Creates a random temporary file. 57 | /// 58 | /// The file system. 59 | /// The file extension. No '.'. 60 | /// . 61 | public static ScopedFile CreateRandomWithExtension(IFileSystem fileSystem, string extension) 62 | => Create(fileSystem, GetRandomFileNameWithExtension(extension)); 63 | 64 | /// 65 | /// Gets the path to a temporary file with the specified file name. 66 | /// 67 | /// The file system. 68 | /// The name of the file. 69 | /// A path to a temporary file. 70 | public static string GetPathWithFileName(IFileSystem fileSystem, string fileName) 71 | { 72 | ArgumentNullException.ThrowIfNull(fileSystem); 73 | 74 | return fileSystem.Path.Combine(TemporaryDirectory.GetPath(fileSystem), fileName); 75 | } 76 | 77 | /// 78 | /// Gets a random file name. 79 | /// 80 | /// A random file name. 81 | public static string GetRandomFileName() => Guid.NewGuid().ToString(); 82 | 83 | /// 84 | /// Gets a random file name with the specified extension. 85 | /// 86 | /// The file extension. No '.'. 87 | /// A random file name with the specified extension. 88 | public static string GetRandomFileNameWithExtension(string extension) 89 | => $"{GetRandomFileName()}.{extension}"; 90 | 91 | /// 92 | /// Gets a path to a random temporary file. The file is not created. 93 | /// 94 | /// The file system. 95 | /// A path to a random temporary file. 96 | public static string GetRandomFilePath(IFileSystem fileSystem) 97 | => GetPathWithFileName(fileSystem, GetRandomFileName()); 98 | 99 | /// 100 | /// Gets a path to a random temporary file. The file is not created. 101 | /// 102 | /// The file system. 103 | /// The file extension. No '.'. 104 | /// A path to a random temporary file. 105 | public static string GetRandomFilePathWithExtension(IFileSystem fileSystem, string extension) 106 | => GetPathWithFileName(fileSystem, GetRandomFileNameWithExtension(extension)); 107 | } 108 | -------------------------------------------------------------------------------- /tests/SlnUp.TestLibrary/WorkingDirectory.cs: -------------------------------------------------------------------------------- 1 | namespace SlnUp.TestLibrary; 2 | 3 | using System.IO.Abstractions; 4 | 5 | using SlnUp.Core; 6 | 7 | internal static class WorkingDirectory 8 | { 9 | /// 10 | /// Gets the current working directory. 11 | /// 12 | /// The file system. 13 | /// The path to the current working directory. 14 | public static string Get(IFileSystem fileSystem) => fileSystem.Directory.GetCurrentDirectory(); 15 | 16 | /// 17 | /// Sets the working directory to the specified path. 18 | /// 19 | /// The file system. 20 | /// The path. 21 | public static void Set(IFileSystem fileSystem, string path) 22 | => fileSystem.Directory.SetCurrentDirectory(path); 23 | 24 | /// 25 | /// Sets the working directory using a to enable reset upon disposal. 26 | /// 27 | /// The file system. 28 | /// The path. 29 | /// . 30 | public static IDisposable SetScoped(IFileSystem fileSystem, string path) 31 | { 32 | string currentPath = Get(fileSystem); 33 | Set(fileSystem, path); 34 | 35 | return new ScopedAction(() => Set(fileSystem, currentPath)); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /tests/SlnUp.Tests/CLI/ArgumentParserTests.cs: -------------------------------------------------------------------------------- 1 | namespace SlnUp.Tests.CLI; 2 | 3 | using System; 4 | using System.CommandLine; 5 | using System.CommandLine.Parsing; 6 | 7 | using SlnUp.CLI; 8 | 9 | public class ArgumentParserTests 10 | { 11 | [Theory] 12 | [InlineData("1.2")] 13 | [InlineData("1.2.3")] 14 | [InlineData("1.2.3.4")] 15 | public void ParseVersion(string expectedVersionValue) 16 | { 17 | // Arrange 18 | Option versionOption = new("--version", ArgumentParser.ParseVersion); 19 | Parser parser = new(new RootCommand() 20 | { 21 | versionOption, 22 | }); 23 | Version expectedVersion = Version.Parse(expectedVersionValue); 24 | 25 | // Act 26 | ParseResult result = parser.Parse($"--version {expectedVersionValue}"); 27 | 28 | // Assert 29 | Assert.Empty(result.Errors); 30 | Version? version = result.RootCommandResult.GetValueForOption(versionOption); 31 | Assert.NotNull(version); 32 | Assert.Equal(expectedVersion, version); 33 | } 34 | 35 | [Fact] 36 | public void ParseVersion_Invalid() 37 | { 38 | // Arrange 39 | Option versionOption = new("--version", ArgumentParser.ParseVersion); 40 | Parser parser = new(new RootCommand() 41 | { 42 | versionOption, 43 | }); 44 | 45 | // Act 46 | ParseResult result = parser.Parse("--version not-valid"); 47 | 48 | // Assert 49 | ParseError error = Assert.Single(result.Errors); 50 | Assert.Equal("version", error.SymbolResult?.Symbol.Name); 51 | Assert.StartsWith("Cannot parse argument 'not-valid'", error.Message, StringComparison.Ordinal); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /tests/SlnUp.Tests/CLI/ProgramOptionsTests.cs: -------------------------------------------------------------------------------- 1 | namespace SlnUp.Tests.CLI; 2 | 3 | using System.CommandLine; 4 | using System.CommandLine.IO; 5 | using System.IO.Abstractions.TestingHelpers; 6 | 7 | using SlnUp.CLI; 8 | using SlnUp.TestLibrary.Extensions; 9 | using SlnUp.Tests.Utilities; 10 | 11 | public class ProgramOptionsTests 12 | { 13 | private readonly TestConsole testConsole = new(); 14 | 15 | [Theory] 16 | [InlineData("2022")] 17 | [InlineData("17.0")] 18 | public void Configure(string version) 19 | { 20 | // Arrange 21 | string[] args = [version]; 22 | 23 | // Act 24 | ProgramOptions? result = this.ConfigureAndInvoke(args, out int exitCode); 25 | 26 | // Assert 27 | Assert.Equal(0, exitCode); 28 | Assert.NotNull(result); 29 | Assert.Equal(version, result.Version); 30 | } 31 | 32 | [Fact] 33 | public void Configure_NoParameters() 34 | { 35 | // Arrange 36 | string[] args = []; 37 | 38 | // Act 39 | ProgramOptions? result = this.ConfigureAndInvoke(args, out int exitCode); 40 | 41 | // Assert 42 | Assert.Equal(0, exitCode); 43 | Assert.NotNull(result); 44 | Assert.Equal(ProgramOptions.DefaultVersionArgument, result.Version); 45 | } 46 | 47 | [Theory] 48 | [InlineData("2022", "--build-version", "17.0.31903.59")] 49 | [InlineData("--build-version", "17.0.31903.59", "2022")] 50 | public void Configure_WithBuildVersion(params string[] args) 51 | { 52 | // Arrange 53 | const string expectedVersion = "2022"; 54 | const string expectedBuildVersion = "17.0.31903.59"; 55 | 56 | // Act 57 | ProgramOptions? result = this.ConfigureAndInvoke(args, out int exitCode); 58 | 59 | // Assert 60 | Assert.Equal(0, exitCode); 61 | Assert.NotNull(result); 62 | Assert.Equal(expectedVersion, result.Version); 63 | Assert.Equal(Version.Parse(expectedBuildVersion), result.BuildVersion); 64 | } 65 | 66 | [Theory] 67 | [InlineData("--help")] 68 | [InlineData("-h")] 69 | [InlineData("-?")] 70 | public void Configure_WithHelp(params string[] args) 71 | { 72 | // Act 73 | ProgramOptions? result = this.ConfigureAndInvoke(args, out int exitCode); 74 | 75 | // Assert 76 | Assert.Equal(0, exitCode); 77 | Assert.Null(result); 78 | Assert.StartsWith("Description:", this.testConsole.GetOutput(), StringComparison.Ordinal); 79 | Assert.True(this.testConsole.HasNoErrorOutput()); 80 | } 81 | 82 | [Fact] 83 | public void Configure_WithInvalidBuildVersion() 84 | { 85 | // Arrange 86 | string[] args = 87 | [ 88 | "2022", 89 | "--build-version", 90 | "invalid-version" 91 | ]; 92 | const string expectedErrorOutput = "Cannot parse argument 'invalid-version' for option 'build-version' as expected type 'System.Version'."; 93 | 94 | // Act 95 | ProgramOptions? result = this.ConfigureAndInvoke(args, out int exitCode); 96 | 97 | // Assert 98 | Assert.Equal(1, exitCode); 99 | Assert.Null(result); 100 | Assert.True(this.testConsole.HasOutput()); 101 | Assert.True(this.testConsole.HasErrorOutput()); 102 | Assert.Equal(expectedErrorOutput, this.testConsole.GetErrorOutput().TrimEnd()); 103 | } 104 | 105 | [Theory] 106 | [InlineData("2022", "--path", "C:/solution.sln")] 107 | [InlineData("2022", "-p", "C:/solution.sln")] 108 | [InlineData("-p", "C:/solution.sln", "2022")] 109 | public void Configure_WithPath(params string[] args) 110 | { 111 | // Arrange 112 | const string expectedVersion = "2022"; 113 | string expectedFilePath = "C:/solution.sln".ToCrossPlatformPath(); 114 | 115 | // Act 116 | ProgramOptions? result = this.ConfigureAndInvoke([.. args.ToCrossPlatformPath()], out int exitCode); 117 | 118 | // Assert 119 | Assert.Equal(0, exitCode); 120 | Assert.NotNull(result); 121 | Assert.Equal(expectedVersion, result.Version); 122 | Assert.Equal(expectedFilePath, result.Path); 123 | } 124 | 125 | [Fact] 126 | public void Configure_WithVersion() 127 | { 128 | // Arrange 129 | string[] args = ["--version"]; 130 | 131 | // Act 132 | ProgramOptions? result = this.ConfigureAndInvoke(args, out int exitCode); 133 | 134 | // Assert 135 | Assert.Equal(0, exitCode); 136 | Assert.Null(result); 137 | Assert.True(this.testConsole.HasOutput()); 138 | } 139 | 140 | /// 141 | /// Minimal invocation: > app 16.8 142 | /// 143 | [Fact] 144 | public void TryGetSlnUpOptions() 145 | { 146 | // Arrange 147 | string expectedSolutionFilePath = "C:\\MyProject.sln".ToCrossPlatformPath(); 148 | ProgramOptions programOptions = new() 149 | { 150 | Version = "16.8" 151 | }; 152 | MockFileSystem fileSystem = new(new Dictionary 153 | { 154 | [expectedSolutionFilePath] = new MockFileData(string.Empty), 155 | }, "C:\\".ToCrossPlatformPath()); 156 | 157 | // Act 158 | bool result = programOptions.TryGetSlnUpOptions(fileSystem, out SlnUpOptions? options); 159 | 160 | // Assert 161 | Assert.True(result); 162 | Assert.NotNull(options); 163 | Assert.Equal(expectedSolutionFilePath, options.SolutionFilePath); 164 | Assert.Equal(Version.Parse("16.8.7"), options.Version); 165 | Assert.Equal(Version.Parse("16.8.31025.109"), options.BuildVersion); 166 | } 167 | 168 | /// 169 | /// app 16.8 --path C:\MyProject.sln 170 | /// 171 | [Fact] 172 | public void TryGetSlnUpOptions_WithAbsoluteSolutionPath() 173 | { 174 | // Arrange 175 | string expectedSolutionFilePath = "C:\\MyProject.sln".ToCrossPlatformPath(); 176 | ProgramOptions programOptions = new() 177 | { 178 | Version = "16.8", 179 | Path = expectedSolutionFilePath 180 | }; 181 | MockFileSystem fileSystem = new(new Dictionary 182 | { 183 | [expectedSolutionFilePath] = new MockFileData(string.Empty), 184 | }, "C:\\".ToCrossPlatformPath()); 185 | 186 | // Act 187 | bool result = programOptions.TryGetSlnUpOptions(fileSystem, out SlnUpOptions? options); 188 | 189 | // Assert 190 | Assert.True(result); 191 | Assert.NotNull(options); 192 | Assert.Equal(expectedSolutionFilePath, options.SolutionFilePath); 193 | } 194 | 195 | /// 196 | /// Build version: > app 0.0 --build-version 0.0.0.0 197 | /// 198 | [Fact] 199 | public void TryGetSlnUpOptions_WithBuildVersion() 200 | { 201 | // Arrange 202 | string expectedSolutionFilePath = "C:\\MyProject.sln".ToCrossPlatformPath(); 203 | ProgramOptions programOptions = new() 204 | { 205 | Version = "0.0", 206 | BuildVersion = Version.Parse("0.0.0.0") 207 | }; 208 | MockFileSystem fileSystem = new(new Dictionary 209 | { 210 | [expectedSolutionFilePath] = new MockFileData(string.Empty), 211 | }, "C:\\".ToCrossPlatformPath()); 212 | 213 | // Act 214 | bool result = programOptions.TryGetSlnUpOptions(fileSystem, out SlnUpOptions? options); 215 | 216 | // Assert 217 | Assert.True(result); 218 | Assert.NotNull(options); 219 | Assert.Equal(expectedSolutionFilePath, options.SolutionFilePath); 220 | Assert.Equal(Version.Parse("0.0"), options.Version); 221 | Assert.Equal(Version.Parse("0.0.0.0"), options.BuildVersion); 222 | } 223 | 224 | /// 225 | /// Build version with invalid version: > app 0 --build-version 0.0.0 226 | /// 227 | [Fact] 228 | public void TryGetSlnUpOptions_WithBuildVersionAndInvalidVersion() 229 | { 230 | // Arrange 231 | string expectedSolutionFilePath = "C:\\MyProject.sln".ToCrossPlatformPath(); 232 | ProgramOptions programOptions = new() 233 | { 234 | Version = "0", 235 | BuildVersion = Version.Parse("0.0.0.0") 236 | }; 237 | MockFileSystem fileSystem = new(new Dictionary 238 | { 239 | [expectedSolutionFilePath] = new MockFileData(string.Empty), 240 | }, "C:\\".ToCrossPlatformPath()); 241 | 242 | // Act 243 | bool result = programOptions.TryGetSlnUpOptions(fileSystem, out SlnUpOptions? options); 244 | 245 | // Assert 246 | Assert.False(result); 247 | Assert.Null(options); 248 | } 249 | 250 | /// 251 | /// Invalid build version: > app 0.0 --build-version 0.0.0 252 | /// 253 | [Fact] 254 | public void TryGetSlnUpOptions_WithInvalidBuildVersion() 255 | { 256 | // Arrange 257 | string expectedSolutionFilePath = "C:\\MyProject.sln".ToCrossPlatformPath(); 258 | ProgramOptions programOptions = new() 259 | { 260 | Version = "0.0", 261 | BuildVersion = Version.Parse("0.0.0") 262 | }; 263 | MockFileSystem fileSystem = new(new Dictionary 264 | { 265 | [expectedSolutionFilePath] = new MockFileData(string.Empty), 266 | }, "C:\\".ToCrossPlatformPath()); 267 | 268 | // Act 269 | bool result = programOptions.TryGetSlnUpOptions(fileSystem, out SlnUpOptions? options); 270 | 271 | // Assert 272 | Assert.False(result); 273 | Assert.Null(options); 274 | } 275 | 276 | /// 277 | /// Minimal invocation: > app 0.0 278 | /// 279 | [Fact] 280 | public void TryGetSlnUpOptions_WithInvalidVersion() 281 | { 282 | // Arrange 283 | string expectedSolutionFilePath = "C:\\MyProject.sln".ToCrossPlatformPath(); 284 | ProgramOptions programOptions = new() 285 | { 286 | Version = "0.0" 287 | }; 288 | MockFileSystem fileSystem = new(new Dictionary 289 | { 290 | [expectedSolutionFilePath] = new MockFileData(string.Empty), 291 | }, "C:\\".ToCrossPlatformPath()); 292 | 293 | // Act 294 | bool result = programOptions.TryGetSlnUpOptions(fileSystem, out SlnUpOptions? options); 295 | 296 | // Assert 297 | Assert.False(result); 298 | Assert.Null(options); 299 | } 300 | 301 | /// 302 | /// Multiple solution files available: > app 16.8 303 | /// 304 | [Fact] 305 | public void TryGetSlnUpOptions_WithMultipleSolutions() 306 | { 307 | // Arrange 308 | ProgramOptions programOptions = new() 309 | { 310 | Version = "16.8" 311 | }; 312 | MockFileSystem fileSystem = new(new Dictionary 313 | { 314 | ["C:\\MyProject.sln".ToCrossPlatformPath()] = new MockFileData(string.Empty), 315 | ["C:\\MyProject2.sln".ToCrossPlatformPath()] = new MockFileData(string.Empty), 316 | }, "C:\\".ToCrossPlatformPath()); 317 | 318 | // Act 319 | bool result = programOptions.TryGetSlnUpOptions(fileSystem, out SlnUpOptions? options); 320 | 321 | // Assert 322 | Assert.False(result); 323 | Assert.Null(options); 324 | } 325 | 326 | /// 327 | /// app 16.8 --path C:\Missing.sln 328 | /// 329 | [Fact] 330 | public void TryGetSlnUpOptions_WithNonExistentSolutionPath() 331 | { 332 | // Arrange 333 | ProgramOptions programOptions = new() 334 | { 335 | Version = "16.8", 336 | Path = "C:\\Missing.sln".ToCrossPlatformPath(), 337 | }; 338 | MockFileSystem fileSystem = new(new Dictionary 339 | { 340 | ["C:\\MyProject.sln".ToCrossPlatformPath()] = new MockFileData(string.Empty), 341 | }, "C:\\".ToCrossPlatformPath()); 342 | 343 | // Act 344 | bool result = programOptions.TryGetSlnUpOptions(fileSystem, out SlnUpOptions? options); 345 | 346 | // Assert 347 | Assert.False(result); 348 | Assert.Null(options); 349 | } 350 | 351 | /// 352 | /// Multiple solution files available: > app 16.8 353 | /// 354 | [Fact] 355 | public void TryGetSlnUpOptions_WithNoSolutions() 356 | { 357 | // Arrange 358 | ProgramOptions programOptions = new() 359 | { 360 | Version = "16.8" 361 | }; 362 | MockFileSystem fileSystem = new(); 363 | 364 | // Act 365 | bool result = programOptions.TryGetSlnUpOptions(fileSystem, out SlnUpOptions? options); 366 | 367 | // Assert 368 | Assert.False(result); 369 | Assert.Null(options); 370 | } 371 | 372 | /// 373 | /// app 16.8 --path .\MyProject.sln 374 | /// 375 | [Fact] 376 | public void TryGetSlnUpOptions_WithRelativeSolutionPath() 377 | { 378 | // Arrange 379 | string expectedSolutionFilePath = "C:\\MyProject.sln".ToCrossPlatformPath(); 380 | ProgramOptions programOptions = new() 381 | { 382 | Version = "16.8", 383 | Path = ".\\MyProject.sln".ToCrossPlatformPath(), 384 | }; 385 | MockFileSystem fileSystem = new(new Dictionary 386 | { 387 | [expectedSolutionFilePath] = new MockFileData(string.Empty), 388 | }, "C:\\".ToCrossPlatformPath()); 389 | 390 | // Act 391 | bool result = programOptions.TryGetSlnUpOptions(fileSystem, out SlnUpOptions? options); 392 | 393 | // Assert 394 | Assert.True(result); 395 | Assert.NotNull(options); 396 | Assert.Equal(expectedSolutionFilePath, options.SolutionFilePath); 397 | } 398 | 399 | private ProgramOptions? ConfigureAndInvoke(string[] args, out int exitCode) 400 | { 401 | ProgramOptions? options = null; 402 | 403 | exitCode = ProgramOptions.Configure(opts => 404 | { 405 | options = opts; 406 | return 0; 407 | }).Invoke(args, this.testConsole); 408 | 409 | return options; 410 | } 411 | } 412 | -------------------------------------------------------------------------------- /tests/SlnUp.Tests/ProgramTests.cs: -------------------------------------------------------------------------------- 1 | namespace SlnUp.Tests; 2 | 3 | using SlnUp; 4 | using SlnUp.TestLibrary; 5 | 6 | public class ProgramTests 7 | { 8 | private const int FailedExitCode = 1; 9 | 10 | private const int NormalExitCode = 0; 11 | 12 | [Fact] 13 | public void Main_NoLocalFile() 14 | { 15 | // Arrange 16 | using ScopedDirectory directory = TemporaryDirectory.CreateRandom(); 17 | string[] args = ["2022"]; 18 | using IDisposable _ = directory.SetAsScopedWorkingDirectory(); 19 | 20 | // Act 21 | int exitCode = Program.Main(args); 22 | 23 | // Assert 24 | Assert.Equal(FailedExitCode, exitCode); 25 | } 26 | 27 | [Fact] 28 | public void Main_WithFilePath() 29 | { 30 | // Arrange 31 | using ScopedFile file = TemporaryFile.CreateRandomWithExtension("sln"); 32 | SolutionFileBuilder solutionFileBuilder = new(Version.Parse("16.0.30114.105")); 33 | SolutionFile solutionFile = solutionFileBuilder.BuildToSolutionFile(file.FileSystem, file.Path); 34 | string[] args = 35 | [ 36 | "2022", 37 | "--path", 38 | file.Path, 39 | ]; 40 | 41 | // Act 42 | int exitCode = Program.Main(args); 43 | 44 | // Assert 45 | Assert.Equal(NormalExitCode, exitCode); 46 | solutionFile.Reload(); 47 | Assert.Equal(17, solutionFile.FileHeader.LastVisualStudioMajorVersion); 48 | } 49 | 50 | [Fact] 51 | public void Main_WithInvalidFile() 52 | { 53 | // Arrange 54 | using ScopedFile file = TemporaryFile.CreateRandomWithExtension("sln"); 55 | string[] args = 56 | [ 57 | "2022", 58 | "--path", 59 | file.Path, 60 | ]; 61 | 62 | // Act 63 | int exitCode = Program.Main(args); 64 | 65 | // Assert 66 | Assert.Equal(FailedExitCode, exitCode); 67 | } 68 | 69 | [Fact] 70 | public void Main_WithLocalFile() 71 | { 72 | // Arrange 73 | using ScopedDirectory directory = TemporaryDirectory.CreateRandom(); 74 | string solutionFilePath = directory.GetRandomFilePathWithExtension("sln"); 75 | SolutionFileBuilder solutionFileBuilder = new(Version.Parse("16.0.30114.105")); 76 | SolutionFile solutionFile = solutionFileBuilder.BuildToSolutionFile(directory.FileSystem, solutionFilePath); 77 | string[] args = ["2022"]; 78 | using IDisposable _ = directory.SetAsScopedWorkingDirectory(); 79 | 80 | // Act 81 | int exitCode = Program.Main(args); 82 | 83 | // Assert 84 | Assert.Equal(NormalExitCode, exitCode); 85 | solutionFile.Reload(); 86 | Assert.Equal(17, solutionFile.FileHeader.LastVisualStudioMajorVersion); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /tests/SlnUp.Tests/SlnUp.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net9.0;net8.0 5 | Exe 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | all 16 | runtime; build; native; contentfiles; analyzers; buildtransitive 17 | 18 | 19 | all 20 | runtime; build; native; contentfiles; analyzers 21 | 22 | 23 | 24 | 25 | 26 | 27 | all 28 | runtime; build; native; contentfiles; analyzers; buildtransitive 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /tests/SlnUp.Tests/SolutionFileBuilder.cs: -------------------------------------------------------------------------------- 1 | namespace SlnUp.Tests; 2 | 3 | using System.IO.Abstractions; 4 | using System.IO.Abstractions.TestingHelpers; 5 | using System.Text; 6 | 7 | using SlnUp.TestLibrary; 8 | 9 | internal sealed class SolutionFileBuilder 10 | { 11 | public static readonly Version DefaultVisualStudioFullVersion = Version.Parse("16.0.28701.123"); 12 | 13 | public static readonly Version DefaultVisualStudioMinimumVersion = Version.Parse(SolutionFileHeader.DefaultMinimumVisualStudioVersion); 14 | 15 | private readonly string fileFormatVersion; 16 | 17 | private readonly int iconMajorVersion; 18 | 19 | private readonly Version visualStudioFullVersion; 20 | 21 | private readonly Version visualStudioMinimumVersion; 22 | 23 | private bool includeBody = true; 24 | 25 | private bool includeFileFormatVersion = true; 26 | 27 | private bool includeSolutionIconVersion = true; 28 | 29 | private bool includeVisualStudioFullVersion = true; 30 | 31 | private bool includeVisualStudioMinimumVersion = true; 32 | 33 | /// 34 | /// Initializes a new instance of the class using all default values. 35 | /// 36 | public SolutionFileBuilder() 37 | : this(DefaultVisualStudioFullVersion) { } 38 | 39 | /// 40 | /// Initializes a new instance of the class from a full Visual Studio version 41 | /// and optional file format version and minimum Visual Studio version. 42 | /// 43 | /// The full Visual Studio version. 44 | /// The file format version. 45 | /// The minimum Visual Studio version. 46 | public SolutionFileBuilder(Version visualStudioFullVersion, string fileFormatVersion = SolutionFileHeader.SupportedFileFormatVersion, Version? visualStudioMinimumVersion = null) 47 | : this(fileFormatVersion, visualStudioFullVersion.Major, visualStudioFullVersion, visualStudioMinimumVersion ?? DefaultVisualStudioMinimumVersion) { } 48 | 49 | /// 50 | /// Initializes a new instance of the class. 51 | /// 52 | /// The file format version. 53 | /// The icon major version. 54 | /// The full Visual Studio version. 55 | /// The minimum Visual Studio version. 56 | public SolutionFileBuilder(string fileFormatVersion, int iconMajorVersion, Version visualStudioFullVersion, Version visualStudioMinimumVersion) 57 | { 58 | this.fileFormatVersion = fileFormatVersion; 59 | this.iconMajorVersion = iconMajorVersion; 60 | this.visualStudioFullVersion = visualStudioFullVersion; 61 | this.visualStudioMinimumVersion = visualStudioMinimumVersion; 62 | } 63 | 64 | /// 65 | /// Builds the content of the solution file. 66 | /// 67 | /// . 68 | public string Build() 69 | { 70 | StringBuilder builder = new(); 71 | 72 | builder.AppendLine(); 73 | 74 | if (this.includeFileFormatVersion) 75 | { 76 | builder.Append("Microsoft Visual Studio Solution File, Format Version ") 77 | .AppendLine(this.fileFormatVersion); 78 | } 79 | 80 | if (this.includeSolutionIconVersion) 81 | { 82 | if (this.iconMajorVersion >= 16) 83 | { 84 | builder.Append("# Visual Studio Version ").Append(this.iconMajorVersion).AppendLine(); 85 | } 86 | else 87 | { 88 | builder.Append("# Visual Studio ").Append(this.iconMajorVersion).AppendLine(); 89 | } 90 | } 91 | 92 | if (this.includeVisualStudioFullVersion) 93 | { 94 | builder.Append("VisualStudioVersion = ").Append(this.visualStudioFullVersion).AppendLine(); 95 | } 96 | 97 | if (this.includeVisualStudioMinimumVersion) 98 | { 99 | builder.Append("MinimumVisualStudioVersion = ").Append(this.visualStudioMinimumVersion).AppendLine(); 100 | } 101 | 102 | if (this.includeBody) 103 | { 104 | builder.AppendLine(@" 105 | Global 106 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 107 | Debug|Any CPU = Debug|Any CPU 108 | Release|Any CPU = Release|Any CPU 109 | EndGlobalSection 110 | GlobalSection(SolutionProperties) = preSolution 111 | HideSolutionNode = FALSE 112 | EndGlobalSection 113 | EndGlobal 114 | ".Trim()); 115 | } 116 | 117 | return builder.ToString(); 118 | } 119 | 120 | /// 121 | /// Builds a solution file and write it to the specified file path in the specified file system. 122 | /// 123 | /// The file system. 124 | /// The file path. 125 | public void BuildToFile(IFileSystem fileSystem, string filePath) 126 | => fileSystem.File.WriteAllText(filePath, this.Build()); 127 | 128 | /// 129 | /// Builds the content of the solution file and puts it into a . 130 | /// 131 | /// The file path. 132 | /// . 133 | public MockFileSystem BuildToMockFileSystem(out string filePath) 134 | { 135 | MockFileSystem fileSystem = new(); 136 | filePath = TemporaryFile.GetRandomFilePathWithExtension(fileSystem, "sln"); 137 | this.BuildToFile(fileSystem, filePath); 138 | 139 | return fileSystem; 140 | } 141 | 142 | /// 143 | /// Builds the content to a . 144 | /// 145 | /// . 146 | public SolutionFile BuildToMockSolutionFile() 147 | { 148 | IFileSystem fileSystem = this.BuildToMockFileSystem(out string filePath); 149 | SolutionFile solutionFile = new(fileSystem, filePath); 150 | 151 | return solutionFile; 152 | } 153 | 154 | /// 155 | /// Builds the content to a . 156 | /// 157 | /// The file system. 158 | /// The file path. 159 | /// . 160 | public SolutionFile BuildToSolutionFile(IFileSystem fileSystem, string filePath) 161 | { 162 | this.BuildToFile(fileSystem, filePath); 163 | SolutionFile solutionFile = new(fileSystem, filePath); 164 | 165 | return solutionFile; 166 | } 167 | 168 | /// 169 | /// Configures the minimum file header lacking the solution icon version and full and minimum Visual Studio versions. 170 | /// 171 | /// . 172 | public SolutionFileBuilder ConfigureMinimumHeader() 173 | { 174 | this.includeSolutionIconVersion = false; 175 | this.includeVisualStudioFullVersion = false; 176 | this.includeVisualStudioMinimumVersion = false; 177 | 178 | return this; 179 | } 180 | 181 | /// 182 | /// Excludes the body from the generated file content. 183 | /// 184 | /// . 185 | public SolutionFileBuilder ExcludeBody() 186 | { 187 | this.includeBody = false; 188 | 189 | return this; 190 | } 191 | 192 | /// 193 | /// Excludes the file format version from the generated file content. 194 | /// 195 | /// . 196 | public SolutionFileBuilder ExcludeFileFormatVersion() 197 | { 198 | this.includeFileFormatVersion = false; 199 | 200 | return this; 201 | } 202 | 203 | /// 204 | /// Excludes the solution icon version from the generated file content. 205 | /// 206 | /// . 207 | public SolutionFileBuilder ExcludeSolutionIconVersion() 208 | { 209 | this.includeSolutionIconVersion = false; 210 | 211 | return this; 212 | } 213 | 214 | /// 215 | /// Excludes the full Visual Studio version from the generated file content. 216 | /// 217 | /// . 218 | public SolutionFileBuilder ExcludeVisualStudioFullVersion() 219 | { 220 | this.includeVisualStudioFullVersion = false; 221 | 222 | return this; 223 | } 224 | 225 | /// 226 | /// Excludes the minimum Visual Studio version from the generated file content. 227 | /// 228 | /// . 229 | public SolutionFileBuilder ExcludeVisualStudioMinimumVersion() 230 | { 231 | this.includeVisualStudioMinimumVersion = false; 232 | 233 | return this; 234 | } 235 | } 236 | -------------------------------------------------------------------------------- /tests/SlnUp.Tests/SolutionFileBuilderTests.cs: -------------------------------------------------------------------------------- 1 | namespace SlnUp.Tests; 2 | 3 | public class SolutionFileBuilderTests 4 | { 5 | [Fact] 6 | public void Build() 7 | { 8 | // Arrange 9 | SolutionFileBuilder fileBuilder = new(); 10 | const string expectedContent = @" 11 | Microsoft Visual Studio Solution File, Format Version 12.00 12 | # Visual Studio Version 16 13 | VisualStudioVersion = 16.0.28701.123 14 | MinimumVisualStudioVersion = 10.0.40219.1 15 | Global 16 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 17 | Debug|Any CPU = Debug|Any CPU 18 | Release|Any CPU = Release|Any CPU 19 | EndGlobalSection 20 | GlobalSection(SolutionProperties) = preSolution 21 | HideSolutionNode = FALSE 22 | EndGlobalSection 23 | EndGlobal 24 | "; 25 | 26 | // Act 27 | string actualContent = fileBuilder.Build(); 28 | 29 | // Assert 30 | Assert.Equal(expectedContent, actualContent); 31 | } 32 | 33 | [Fact] 34 | public void Build_ForVS15() 35 | { 36 | // Arrange 37 | SolutionFileBuilder fileBuilder = new(Version.Parse("15.0.28701.123")); 38 | const string expectedContent = @" 39 | Microsoft Visual Studio Solution File, Format Version 12.00 40 | # Visual Studio 15 41 | VisualStudioVersion = 15.0.28701.123 42 | MinimumVisualStudioVersion = 10.0.40219.1 43 | Global 44 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 45 | Debug|Any CPU = Debug|Any CPU 46 | Release|Any CPU = Release|Any CPU 47 | EndGlobalSection 48 | GlobalSection(SolutionProperties) = preSolution 49 | HideSolutionNode = FALSE 50 | EndGlobalSection 51 | EndGlobal 52 | "; 53 | 54 | // Act 55 | string actualContent = fileBuilder.Build(); 56 | 57 | // Assert 58 | Assert.Equal(expectedContent, actualContent); 59 | } 60 | 61 | [Fact] 62 | public void Build_WithMinimumHeader() 63 | { 64 | // Arrange 65 | SolutionFileBuilder fileBuilder = new SolutionFileBuilder() 66 | .ConfigureMinimumHeader(); 67 | const string expectedContent = @" 68 | Microsoft Visual Studio Solution File, Format Version 12.00 69 | Global 70 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 71 | Debug|Any CPU = Debug|Any CPU 72 | Release|Any CPU = Release|Any CPU 73 | EndGlobalSection 74 | GlobalSection(SolutionProperties) = preSolution 75 | HideSolutionNode = FALSE 76 | EndGlobalSection 77 | EndGlobal 78 | "; 79 | 80 | // Act 81 | string actualContent = fileBuilder.Build(); 82 | 83 | // Assert 84 | Assert.Equal(expectedContent, actualContent); 85 | } 86 | 87 | [Fact] 88 | public void Build_WithoutBody() 89 | { 90 | // Arrange 91 | SolutionFileBuilder fileBuilder = new SolutionFileBuilder() 92 | .ExcludeBody(); 93 | const string expectedContent = @" 94 | Microsoft Visual Studio Solution File, Format Version 12.00 95 | # Visual Studio Version 16 96 | VisualStudioVersion = 16.0.28701.123 97 | MinimumVisualStudioVersion = 10.0.40219.1 98 | "; 99 | 100 | // Act 101 | string actualContent = fileBuilder.Build(); 102 | 103 | // Assert 104 | Assert.Equal(expectedContent, actualContent); 105 | } 106 | 107 | [Fact] 108 | public void Build_WithoutFileFormatVersion() 109 | { 110 | // Arrange 111 | SolutionFileBuilder fileBuilder = new SolutionFileBuilder() 112 | .ExcludeFileFormatVersion(); 113 | const string expectedContent = @" 114 | # Visual Studio Version 16 115 | VisualStudioVersion = 16.0.28701.123 116 | MinimumVisualStudioVersion = 10.0.40219.1 117 | Global 118 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 119 | Debug|Any CPU = Debug|Any CPU 120 | Release|Any CPU = Release|Any CPU 121 | EndGlobalSection 122 | GlobalSection(SolutionProperties) = preSolution 123 | HideSolutionNode = FALSE 124 | EndGlobalSection 125 | EndGlobal 126 | "; 127 | 128 | // Act 129 | string actualContent = fileBuilder.Build(); 130 | 131 | // Assert 132 | Assert.Equal(expectedContent, actualContent); 133 | } 134 | 135 | [Fact] 136 | public void Build_WithoutSolutionIconVersion() 137 | { 138 | // Arrange 139 | SolutionFileBuilder fileBuilder = new SolutionFileBuilder() 140 | .ExcludeSolutionIconVersion(); 141 | const string expectedContent = @" 142 | Microsoft Visual Studio Solution File, Format Version 12.00 143 | VisualStudioVersion = 16.0.28701.123 144 | MinimumVisualStudioVersion = 10.0.40219.1 145 | Global 146 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 147 | Debug|Any CPU = Debug|Any CPU 148 | Release|Any CPU = Release|Any CPU 149 | EndGlobalSection 150 | GlobalSection(SolutionProperties) = preSolution 151 | HideSolutionNode = FALSE 152 | EndGlobalSection 153 | EndGlobal 154 | "; 155 | 156 | // Act 157 | string actualContent = fileBuilder.Build(); 158 | 159 | // Assert 160 | Assert.Equal(expectedContent, actualContent); 161 | } 162 | 163 | [Fact] 164 | public void Build_WithoutVisualStudioFullVersion() 165 | { 166 | // Arrange 167 | SolutionFileBuilder fileBuilder = new SolutionFileBuilder() 168 | .ExcludeVisualStudioFullVersion(); 169 | const string expectedContent = @" 170 | Microsoft Visual Studio Solution File, Format Version 12.00 171 | # Visual Studio Version 16 172 | MinimumVisualStudioVersion = 10.0.40219.1 173 | Global 174 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 175 | Debug|Any CPU = Debug|Any CPU 176 | Release|Any CPU = Release|Any CPU 177 | EndGlobalSection 178 | GlobalSection(SolutionProperties) = preSolution 179 | HideSolutionNode = FALSE 180 | EndGlobalSection 181 | EndGlobal 182 | "; 183 | 184 | // Act 185 | string actualContent = fileBuilder.Build(); 186 | 187 | // Assert 188 | Assert.Equal(expectedContent, actualContent); 189 | } 190 | 191 | [Fact] 192 | public void Build_WithoutVisualStudioMinimumVersion() 193 | { 194 | // Arrange 195 | SolutionFileBuilder fileBuilder = new SolutionFileBuilder() 196 | .ExcludeVisualStudioMinimumVersion(); 197 | const string expectedContent = @" 198 | Microsoft Visual Studio Solution File, Format Version 12.00 199 | # Visual Studio Version 16 200 | VisualStudioVersion = 16.0.28701.123 201 | Global 202 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 203 | Debug|Any CPU = Debug|Any CPU 204 | Release|Any CPU = Release|Any CPU 205 | EndGlobalSection 206 | GlobalSection(SolutionProperties) = preSolution 207 | HideSolutionNode = FALSE 208 | EndGlobalSection 209 | EndGlobal 210 | "; 211 | 212 | // Act 213 | string actualContent = fileBuilder.Build(); 214 | 215 | // Assert 216 | Assert.Equal(expectedContent, actualContent); 217 | } 218 | } 219 | -------------------------------------------------------------------------------- /tests/SlnUp.Tests/SolutionFileHeaderTests.cs: -------------------------------------------------------------------------------- 1 | namespace SlnUp.Tests; 2 | 3 | public class SolutionFileHeaderTests 4 | { 5 | [Fact] 6 | public void Construct() 7 | { 8 | // Arrange 9 | Version version = Version.Parse("17.0.31903.59"); 10 | 11 | // Act 12 | SolutionFileHeader fileHeader = new( 13 | SolutionFileHeader.SupportedFileFormatVersion, 14 | version.Major, 15 | version, 16 | Version.Parse(SolutionFileHeader.DefaultMinimumVisualStudioVersion)); 17 | 18 | // Assert 19 | Assert.Equal(SolutionFileHeader.SupportedFileFormatVersion, fileHeader.FileFormatVersion); 20 | Assert.Equal(version.Major, fileHeader.LastVisualStudioMajorVersion); 21 | Assert.Equal(version, fileHeader.LastVisualStudioVersion); 22 | Assert.Equal(Version.Parse(SolutionFileHeader.DefaultMinimumVisualStudioVersion), fileHeader.MinimumVisualStudioVersion); 23 | } 24 | 25 | [Fact] 26 | public void Construct_WithUnsupportedFileFormatVersion() 27 | { 28 | // Arrange and act 29 | ArgumentException ex = Assert.Throws(() => new SolutionFileHeader("13.00")); 30 | 31 | // Assert 32 | Assert.Equal("fileFormatVersion", ex.ParamName); 33 | } 34 | 35 | [Fact] 36 | public void DuplicateAndUpdate() 37 | { 38 | // Arrange 39 | Version originalVersion = Version.Parse("17.0.31903.59"); 40 | SolutionFileHeader originalFileHeader = new( 41 | SolutionFileHeader.SupportedFileFormatVersion, 42 | originalVersion.Major, 43 | originalVersion, 44 | originalVersion); 45 | Version updatedVersion = Version.Parse("17.1.32210.238"); 46 | 47 | // Act 48 | SolutionFileHeader updatedFileHeader = originalFileHeader.DuplicateAndUpdate(updatedVersion); 49 | 50 | // Assert 51 | Assert.NotSame(originalFileHeader, updatedFileHeader); 52 | Assert.Equal(SolutionFileHeader.SupportedFileFormatVersion, updatedFileHeader.FileFormatVersion); 53 | Assert.Equal(updatedVersion.Major, updatedFileHeader.LastVisualStudioMajorVersion); 54 | Assert.Equal(updatedVersion, updatedFileHeader.LastVisualStudioVersion); 55 | Assert.Equal(originalVersion, updatedFileHeader.MinimumVisualStudioVersion); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /tests/SlnUp.Tests/SolutionFileTests.cs: -------------------------------------------------------------------------------- 1 | namespace SlnUp.Tests; 2 | 3 | using System.IO.Abstractions.TestingHelpers; 4 | 5 | using SlnUp.TestLibrary; 6 | 7 | public class SolutionFileTests 8 | { 9 | [Fact] 10 | public void Construct_WithEmptyFile() 11 | { 12 | // Arrange 13 | const string filePath = "C:\\MyProject.sln"; 14 | MockFileSystem fileSystem = new(new Dictionary 15 | { 16 | [filePath] = new MockFileData(string.Empty) 17 | }); 18 | 19 | // Act and assert 20 | Assert.Throws(() => new SolutionFile(fileSystem, filePath)); 21 | } 22 | 23 | [Fact] 24 | public void Construct_WithFullHeader() 25 | { 26 | // Arrange 27 | Version expectedVersion = Version.Parse("16.0.30114.105"); 28 | MockFileSystem fileSystem = new SolutionFileBuilder(expectedVersion) 29 | .BuildToMockFileSystem(out string filePath); 30 | 31 | // Act 32 | SolutionFile solutionFile = new(fileSystem, filePath); 33 | 34 | // Assert 35 | SolutionFileHeader fileHeader = solutionFile.FileHeader; 36 | Assert.Equal(SolutionFileHeader.SupportedFileFormatVersion, fileHeader.FileFormatVersion); 37 | Assert.Equal(16, fileHeader.LastVisualStudioMajorVersion); 38 | Assert.Equal(expectedVersion, fileHeader.LastVisualStudioVersion); 39 | Assert.Equal(SolutionFileBuilder.DefaultVisualStudioMinimumVersion, fileHeader.MinimumVisualStudioVersion); 40 | } 41 | 42 | [Fact] 43 | public void Construct_WithFullV15Header() 44 | { 45 | // Arrange 46 | Version expectedVersion = Version.Parse("15.0.26124.0"); 47 | MockFileSystem fileSystem = new SolutionFileBuilder(expectedVersion, visualStudioMinimumVersion: expectedVersion) 48 | .BuildToMockFileSystem(out string filePath); 49 | 50 | // Act 51 | SolutionFile solutionFile = new(fileSystem, filePath); 52 | 53 | // Assert 54 | SolutionFileHeader fileHeader = solutionFile.FileHeader; 55 | Assert.Equal(SolutionFileHeader.SupportedFileFormatVersion, fileHeader.FileFormatVersion); 56 | Assert.Equal(15, fileHeader.LastVisualStudioMajorVersion); 57 | Assert.Equal(expectedVersion, fileHeader.LastVisualStudioVersion); 58 | Assert.Equal(expectedVersion, fileHeader.MinimumVisualStudioVersion); 59 | } 60 | 61 | [Fact] 62 | public void Construct_WithMinimalHeader() 63 | { 64 | // Arrange 65 | MockFileSystem fileSystem = new SolutionFileBuilder() 66 | .ConfigureMinimumHeader() 67 | .BuildToMockFileSystem(out string filePath); 68 | 69 | // Act 70 | SolutionFile solutionFile = new(fileSystem, filePath); 71 | 72 | // Assert 73 | SolutionFileHeader fileHeader = solutionFile.FileHeader; 74 | Assert.Equal(SolutionFileHeader.SupportedFileFormatVersion, fileHeader.FileFormatVersion); 75 | Assert.Null(fileHeader.LastVisualStudioMajorVersion); 76 | Assert.Null(fileHeader.LastVisualStudioVersion); 77 | Assert.Null(fileHeader.MinimumVisualStudioVersion); 78 | } 79 | 80 | [Fact] 81 | public void Construct_WithMinimalHeaderNoBody() 82 | { 83 | // Arrange 84 | MockFileSystem fileSystem = new SolutionFileBuilder() 85 | .ConfigureMinimumHeader() 86 | .ExcludeBody() 87 | .BuildToMockFileSystem(out string filePath); 88 | 89 | // Act 90 | SolutionFile solutionFile = new(fileSystem, filePath); 91 | 92 | // Assert 93 | SolutionFileHeader fileHeader = solutionFile.FileHeader; 94 | Assert.Equal(SolutionFileHeader.SupportedFileFormatVersion, fileHeader.FileFormatVersion); 95 | Assert.Null(fileHeader.LastVisualStudioMajorVersion); 96 | Assert.Null(fileHeader.LastVisualStudioVersion); 97 | Assert.Null(fileHeader.MinimumVisualStudioVersion); 98 | } 99 | 100 | [Fact] 101 | public void Construct_WithMissingFile() 102 | { 103 | // Arrange 104 | MockFileSystem fileSystem = new(); 105 | string filePath = TemporaryFile.GetRandomFilePathWithExtension(fileSystem, "sln"); 106 | 107 | // Act and assert 108 | FileNotFoundException exception = Assert.Throws(() => new SolutionFile(fileSystem, filePath)); 109 | 110 | // Assert 111 | Assert.Equal(filePath, exception.FileName); 112 | } 113 | 114 | [Fact] 115 | public void Construct_WithMissingFileFormatVersion() 116 | { 117 | // Arrange 118 | MockFileSystem fileSystem = new SolutionFileBuilder() 119 | .ExcludeFileFormatVersion() 120 | .BuildToMockFileSystem(out string filePath); 121 | 122 | // Act 123 | Assert.Throws(() => new SolutionFile(fileSystem, filePath)); 124 | } 125 | 126 | [Fact] 127 | public void UpdateFileHeader_WithFullHeader() 128 | { 129 | // Arrange 130 | SolutionFile solutionFile = new SolutionFileBuilder(Version.Parse("16.0.30114.105")) 131 | .BuildToMockSolutionFile(); 132 | Version expectedVersion = Version.Parse("17.0.31903.59"); 133 | string expectedContent = new SolutionFileBuilder(expectedVersion).Build(); 134 | 135 | // Act 136 | solutionFile.UpdateFileHeader(expectedVersion); 137 | 138 | // Assert 139 | SolutionFileHeader fileHeader = solutionFile.FileHeader; 140 | Assert.Equal(SolutionFileHeader.SupportedFileFormatVersion, fileHeader.FileFormatVersion); 141 | Assert.Equal(expectedVersion.Major, fileHeader.LastVisualStudioMajorVersion); 142 | Assert.Equal(expectedVersion, fileHeader.LastVisualStudioVersion); 143 | Assert.Equal(SolutionFileBuilder.DefaultVisualStudioMinimumVersion, fileHeader.MinimumVisualStudioVersion); 144 | Assert.Equal(expectedContent, solutionFile.ReadContent()); 145 | } 146 | 147 | [Fact] 148 | public void UpdateFileHeader_WithFullV15Header() 149 | { 150 | // Arrange 151 | SolutionFile solutionFile = new SolutionFileBuilder(Version.Parse("15.0.26124.0")) 152 | .BuildToMockSolutionFile(); 153 | Version expectedVersion = Version.Parse("15.0.27000.0"); 154 | string expectedContent = new SolutionFileBuilder(expectedVersion).Build(); 155 | 156 | // Act 157 | solutionFile.UpdateFileHeader(expectedVersion); 158 | 159 | // Assert 160 | SolutionFileHeader fileHeader = solutionFile.FileHeader; 161 | Assert.Equal(SolutionFileHeader.SupportedFileFormatVersion, fileHeader.FileFormatVersion); 162 | Assert.Equal(expectedVersion.Major, fileHeader.LastVisualStudioMajorVersion); 163 | Assert.Equal(expectedVersion, fileHeader.LastVisualStudioVersion); 164 | Assert.Equal(SolutionFileBuilder.DefaultVisualStudioMinimumVersion, fileHeader.MinimumVisualStudioVersion); 165 | Assert.Equal(expectedContent, solutionFile.ReadContent()); 166 | } 167 | 168 | [Fact] 169 | public void UpdateFileHeader_WithMinimalHeader() 170 | { 171 | // Arrange 172 | Version expectedVersion = Version.Parse("17.0.31903.59"); 173 | string expectedFileContent = new SolutionFileBuilder(expectedVersion).Build(); 174 | SolutionFile solutionFile = new SolutionFileBuilder() 175 | .ConfigureMinimumHeader() 176 | .BuildToMockSolutionFile(); 177 | 178 | // Act 179 | solutionFile.UpdateFileHeader(expectedVersion); 180 | 181 | // Assert 182 | SolutionFileHeader fileHeader = solutionFile.FileHeader; 183 | Assert.Equal(SolutionFileHeader.SupportedFileFormatVersion, fileHeader.FileFormatVersion); 184 | Assert.Equal(expectedVersion.Major, fileHeader.LastVisualStudioMajorVersion); 185 | Assert.Equal(expectedVersion, fileHeader.LastVisualStudioVersion); 186 | Assert.Equal(Version.Parse(SolutionFileHeader.DefaultMinimumVisualStudioVersion), fileHeader.MinimumVisualStudioVersion); 187 | Assert.Equal(expectedFileContent, solutionFile.ReadContent()); 188 | } 189 | 190 | [Fact] 191 | public void UpdateFileHeader_WithMissingMajorVersion() 192 | { 193 | // Arrange 194 | Version expectedVersion = Version.Parse("17.0.31903.59"); 195 | string expectedFileContent = new SolutionFileBuilder(expectedVersion).Build(); 196 | SolutionFile solutionFile = new SolutionFileBuilder(expectedVersion) 197 | .ExcludeSolutionIconVersion() 198 | .BuildToMockSolutionFile(); 199 | 200 | // Act 201 | solutionFile.UpdateFileHeader(expectedVersion); 202 | 203 | // Assert 204 | SolutionFileHeader fileHeader = solutionFile.FileHeader; 205 | Assert.Equal(SolutionFileHeader.SupportedFileFormatVersion, fileHeader.FileFormatVersion); 206 | Assert.Equal(expectedVersion.Major, fileHeader.LastVisualStudioMajorVersion); 207 | Assert.Equal(expectedVersion, fileHeader.LastVisualStudioVersion); 208 | Assert.Equal(Version.Parse(SolutionFileHeader.DefaultMinimumVisualStudioVersion), fileHeader.MinimumVisualStudioVersion); 209 | Assert.Equal(expectedFileContent, solutionFile.ReadContent()); 210 | } 211 | 212 | [Fact] 213 | public void UpdateFileHeader_WithMissingMinimumVersion() 214 | { 215 | // Arrange 216 | Version expectedVersion = Version.Parse("17.0.31903.59"); 217 | string expectedFileContent = new SolutionFileBuilder(expectedVersion).Build(); 218 | SolutionFile solutionFile = new SolutionFileBuilder(expectedVersion) 219 | .ExcludeVisualStudioMinimumVersion() 220 | .BuildToMockSolutionFile(); 221 | 222 | // Act 223 | solutionFile.UpdateFileHeader(expectedVersion); 224 | 225 | // Assert 226 | SolutionFileHeader fileHeader = solutionFile.FileHeader; 227 | Assert.Equal(SolutionFileHeader.SupportedFileFormatVersion, fileHeader.FileFormatVersion); 228 | Assert.Equal(expectedVersion.Major, fileHeader.LastVisualStudioMajorVersion); 229 | Assert.Equal(expectedVersion, fileHeader.LastVisualStudioVersion); 230 | Assert.Equal(Version.Parse(SolutionFileHeader.DefaultMinimumVisualStudioVersion), fileHeader.MinimumVisualStudioVersion); 231 | Assert.Equal(expectedFileContent, solutionFile.ReadContent()); 232 | } 233 | 234 | [Fact] 235 | public void UpdateFileHeader_WithMissingVersion() 236 | { 237 | // Arrange 238 | Version expectedVersion = Version.Parse("17.0.31903.59"); 239 | string expectedFileContent = new SolutionFileBuilder(expectedVersion).Build(); 240 | SolutionFile solutionFile = new SolutionFileBuilder(expectedVersion) 241 | .ExcludeVisualStudioFullVersion() 242 | .BuildToMockSolutionFile(); 243 | 244 | // Act 245 | solutionFile.UpdateFileHeader(expectedVersion); 246 | 247 | // Assert 248 | SolutionFileHeader fileHeader = solutionFile.FileHeader; 249 | Assert.Equal(SolutionFileHeader.SupportedFileFormatVersion, fileHeader.FileFormatVersion); 250 | Assert.Equal(expectedVersion.Major, fileHeader.LastVisualStudioMajorVersion); 251 | Assert.Equal(expectedVersion, fileHeader.LastVisualStudioVersion); 252 | Assert.Equal(Version.Parse(SolutionFileHeader.DefaultMinimumVisualStudioVersion), fileHeader.MinimumVisualStudioVersion); 253 | Assert.Equal(expectedFileContent, solutionFile.ReadContent()); 254 | } 255 | 256 | [Fact] 257 | public void UpdateFileHeader_WithNullLastVisualStudioMajorVersion() 258 | { 259 | // Arrange 260 | Version expectedVersion = Version.Parse("17.0.31903.59"); 261 | SolutionFile solutionFile = new SolutionFileBuilder(expectedVersion).BuildToMockSolutionFile(); 262 | SolutionFileHeader fileHeader = new( 263 | fileFormatVersion: SolutionFileHeader.SupportedFileFormatVersion, 264 | lastVisualStudioMajorVersion: null, 265 | lastVisualStudioVersion: expectedVersion, 266 | minimumVisualStudioVersion: expectedVersion); 267 | 268 | // Act and assert 269 | Assert.Throws(() => solutionFile.UpdateFileHeader(fileHeader)); 270 | } 271 | 272 | [Fact] 273 | public void UpdateFileHeader_WithNullLastVisualStudioVersion() 274 | { 275 | // Arrange 276 | Version expectedVersion = Version.Parse("17.0.31903.59"); 277 | SolutionFile solutionFile = new SolutionFileBuilder(expectedVersion).BuildToMockSolutionFile(); 278 | SolutionFileHeader fileHeader = new( 279 | fileFormatVersion: SolutionFileHeader.SupportedFileFormatVersion, 280 | lastVisualStudioMajorVersion: expectedVersion.Major, 281 | lastVisualStudioVersion: null, 282 | minimumVisualStudioVersion: expectedVersion); 283 | 284 | // Act and assert 285 | Assert.Throws(() => solutionFile.UpdateFileHeader(fileHeader)); 286 | } 287 | } 288 | -------------------------------------------------------------------------------- /tests/SlnUp.Tests/Utilities/TestConsoleExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace SlnUp.Tests.Utilities; 2 | 3 | using System.CommandLine; 4 | 5 | internal static class TestConsoleExtensions 6 | { 7 | public static string GetErrorOutput(this IConsole console) => console.Error.ToString() ?? string.Empty; 8 | 9 | public static string GetOutput(this IConsole console) => console.Out.ToString() ?? string.Empty; 10 | 11 | public static bool HasErrorOutput(this IConsole console) => !string.IsNullOrEmpty(console.GetErrorOutput()); 12 | 13 | public static bool HasNoErrorOutput(this IConsole console) => !console.HasErrorOutput(); 14 | 15 | public static bool HasNoOutput(this IConsole console) => !console.HasOutput(); 16 | 17 | public static bool HasOutput(this IConsole console) => !string.IsNullOrEmpty(console.GetOutput()); 18 | } 19 | -------------------------------------------------------------------------------- /version.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/AArnott/Nerdbank.GitVersioning/master/src/NerdBank.GitVersioning/version.schema.json", 3 | "version": "3.0.1-beta.{height}", 4 | "buildNumberOffset": -1, 5 | "nugetPackageVersion": { 6 | "semVer": 2 7 | }, 8 | "publicReleaseRefSpec": [ 9 | "^refs/heads/main$" // we release out of main 10 | ] 11 | } --------------------------------------------------------------------------------