├── .editorconfig ├── .github ├── funding.yml └── workflows │ ├── ci.yml │ └── pr.yml ├── .gitignore ├── CODE_OF_CONDUCT.md ├── LICENSE.md ├── README.md ├── build.cake ├── dotnet-tools.json ├── global.json ├── res └── screenshot.png └── src ├── .editorconfig ├── Directory.Build.props ├── Directory.Build.targets ├── Verify.Terminal.Tests ├── .editorconfig ├── Data │ ├── First │ │ ├── new │ │ └── old │ └── Second │ │ ├── new │ │ └── old ├── Expectations │ └── Rendering │ │ ├── Render.Output_First.verified.txt │ │ └── Render.Output_Second.verified.txt ├── GlobalUsings.cs ├── Properties │ ├── Usings.cs │ └── VerifyConfig.cs ├── SnapshotFinderTests.cs ├── SnapshotRendererTests.cs ├── Utilities │ ├── EmbeddedResourceReader.cs │ └── FileSystemExtensions.cs └── Verify.Terminal.Tests.csproj ├── Verify.Terminal.sln ├── Verify.Terminal.sln.DotSettings ├── Verify.Terminal ├── Commands │ ├── Modify │ │ ├── AcceptCommand.cs │ │ └── RejectCommand.cs │ ├── ModifyCommand.cs │ └── ReviewCommand.cs ├── Extensions │ ├── ConsoleExtensions.cs │ ├── EnumerableExtensions.cs │ └── FilePathExtensions.cs ├── GlobalUsings.cs ├── Guard.cs ├── Infrastructure │ ├── DirectoryPathConverter.cs │ ├── TypeRegistrar.cs │ └── TypeResolver.cs ├── Program.cs ├── Properties │ └── Usings.cs ├── Rendering │ ├── Character.cs │ ├── CharacterSet.cs │ ├── ReportBuilder.cs │ └── ReportRenderable.cs ├── Snapshot.cs ├── SnapshotDiff.cs ├── SnapshotDiffAction.cs ├── SnapshotDiffer.cs ├── SnapshotFinder.cs ├── SnapshotManager.cs ├── SnapshotRenderer.cs └── Verify.Terminal.csproj └── stylecop.json /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = CRLF 6 | indent_style = space 7 | indent_size = 4 8 | insert_final_newline = false 9 | trim_trailing_whitespace = true 10 | 11 | [*.sln] 12 | indent_style = tab 13 | 14 | [*.{csproj,vbproj,vcxproj,vcxproj.filters}] 15 | indent_size = 2 16 | 17 | [*.{xml,config,props,targets,nuspec,ruleset}] 18 | indent_size = 2 19 | 20 | [*.{yml,yaml}] 21 | indent_size = 2 22 | 23 | [*.json] 24 | indent_size = 2 25 | 26 | [*.md] 27 | trim_trailing_whitespace = false 28 | 29 | [*.sh] 30 | end_of_line = lf 31 | 32 | [*.cs] 33 | # Prefer file scoped namespace declarations 34 | csharp_style_namespace_declarations = file_scoped:warning 35 | 36 | # Sort using and Import directives with System.* appearing first 37 | dotnet_sort_system_directives_first = true 38 | dotnet_separate_import_directive_groups = false 39 | 40 | # Avoid "this." and "Me." if not necessary 41 | dotnet_style_qualification_for_field = false:refactoring 42 | dotnet_style_qualification_for_property = false:refactoring 43 | dotnet_style_qualification_for_method = false:refactoring 44 | dotnet_style_qualification_for_event = false:refactoring 45 | 46 | # Use language keywords instead of framework type names for type references 47 | dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion 48 | dotnet_style_predefined_type_for_member_access = true:suggestion 49 | 50 | # Suggest more modern language features when available 51 | dotnet_style_object_initializer = true:suggestion 52 | dotnet_style_collection_initializer = true:suggestion 53 | dotnet_style_coalesce_expression = true:suggestion 54 | dotnet_style_null_propagation = true:suggestion 55 | dotnet_style_explicit_tuple_names = true:suggestion 56 | 57 | # Non-private static fields are PascalCase 58 | dotnet_naming_rule.non_private_static_fields_should_be_pascal_case.severity = suggestion 59 | dotnet_naming_rule.non_private_static_fields_should_be_pascal_case.symbols = non_private_static_fields 60 | dotnet_naming_rule.non_private_static_fields_should_be_pascal_case.style = non_private_static_field_style 61 | dotnet_naming_symbols.non_private_static_fields.applicable_kinds = field 62 | dotnet_naming_symbols.non_private_static_fields.applicable_accessibilities = public, protected, internal, protected_internal, private_protected 63 | dotnet_naming_symbols.non_private_static_fields.required_modifiers = static 64 | dotnet_naming_style.non_private_static_field_style.capitalization = pascal_case 65 | 66 | # Non-private readonly fields are PascalCase 67 | dotnet_naming_rule.non_private_readonly_fields_should_be_pascal_case.severity = suggestion 68 | dotnet_naming_rule.non_private_readonly_fields_should_be_pascal_case.symbols = non_private_readonly_fields 69 | dotnet_naming_rule.non_private_readonly_fields_should_be_pascal_case.style = non_private_readonly_field_style 70 | dotnet_naming_symbols.non_private_readonly_fields.applicable_kinds = field 71 | dotnet_naming_symbols.non_private_readonly_fields.applicable_accessibilities = public, protected, internal, protected_internal, private_protected 72 | dotnet_naming_symbols.non_private_readonly_fields.required_modifiers = readonly 73 | dotnet_naming_style.non_private_readonly_field_style.capitalization = pascal_case 74 | 75 | # Constants are PascalCase 76 | dotnet_naming_rule.constants_should_be_pascal_case.severity = suggestion 77 | dotnet_naming_rule.constants_should_be_pascal_case.symbols = constants 78 | dotnet_naming_rule.constants_should_be_pascal_case.style = constant_style 79 | dotnet_naming_symbols.constants.applicable_kinds = field, local 80 | dotnet_naming_symbols.constants.required_modifiers = const 81 | dotnet_naming_style.constant_style.capitalization = pascal_case 82 | 83 | # Instance fields are camelCase and start with _ 84 | dotnet_naming_rule.instance_fields_should_be_camel_case.severity = suggestion 85 | dotnet_naming_rule.instance_fields_should_be_camel_case.symbols = instance_fields 86 | dotnet_naming_rule.instance_fields_should_be_camel_case.style = instance_field_style 87 | dotnet_naming_symbols.instance_fields.applicable_kinds = field 88 | dotnet_naming_style.instance_field_style.capitalization = camel_case 89 | dotnet_naming_style.instance_field_style.required_prefix = _ 90 | 91 | # Locals and parameters are camelCase 92 | dotnet_naming_rule.locals_should_be_camel_case.severity = suggestion 93 | dotnet_naming_rule.locals_should_be_camel_case.symbols = locals_and_parameters 94 | dotnet_naming_rule.locals_should_be_camel_case.style = camel_case_style 95 | dotnet_naming_symbols.locals_and_parameters.applicable_kinds = parameter, local 96 | dotnet_naming_style.camel_case_style.capitalization = camel_case 97 | 98 | # Local functions are PascalCase 99 | dotnet_naming_rule.local_functions_should_be_pascal_case.severity = suggestion 100 | dotnet_naming_rule.local_functions_should_be_pascal_case.symbols = local_functions 101 | dotnet_naming_rule.local_functions_should_be_pascal_case.style = local_function_style 102 | dotnet_naming_symbols.local_functions.applicable_kinds = local_function 103 | dotnet_naming_style.local_function_style.capitalization = pascal_case 104 | 105 | # By default, name items with PascalCase 106 | dotnet_naming_rule.members_should_be_pascal_case.severity = suggestion 107 | dotnet_naming_rule.members_should_be_pascal_case.symbols = all_members 108 | dotnet_naming_rule.members_should_be_pascal_case.style = pascal_case_style 109 | dotnet_naming_symbols.all_members.applicable_kinds = * 110 | dotnet_naming_style.pascal_case_style.capitalization = pascal_case 111 | 112 | # Newline settings 113 | csharp_new_line_before_open_brace = all 114 | csharp_new_line_before_else = true 115 | csharp_new_line_before_catch = true 116 | csharp_new_line_before_finally = true 117 | csharp_new_line_before_members_in_object_initializers = true 118 | csharp_new_line_before_members_in_anonymous_types = true 119 | csharp_new_line_between_query_expression_clauses = true 120 | 121 | # Indentation preferences 122 | csharp_indent_block_contents = true 123 | csharp_indent_braces = false 124 | csharp_indent_case_contents = true 125 | csharp_indent_case_contents_when_block = true 126 | csharp_indent_switch_labels = true 127 | csharp_indent_labels = flush_left 128 | 129 | # Prefer "var" everywhere 130 | csharp_style_var_for_built_in_types = true:suggestion 131 | csharp_style_var_when_type_is_apparent = true:suggestion 132 | csharp_style_var_elsewhere = true:suggestion 133 | 134 | # Prefer method-like constructs to have a block body 135 | csharp_style_expression_bodied_methods = false:none 136 | csharp_style_expression_bodied_constructors = false:none 137 | csharp_style_expression_bodied_operators = false:none 138 | 139 | # Prefer property-like constructs to have an expression-body 140 | csharp_style_expression_bodied_properties = true:none 141 | csharp_style_expression_bodied_indexers = true:none 142 | csharp_style_expression_bodied_accessors = true:none 143 | 144 | # Suggest more modern language features when available 145 | csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion 146 | csharp_style_pattern_matching_over_as_with_null_check = true:suggestion 147 | csharp_style_inlined_variable_declaration = true:suggestion 148 | csharp_style_throw_expression = true:suggestion 149 | csharp_style_conditional_delegate_call = true:suggestion 150 | 151 | # Space preferences 152 | csharp_space_after_cast = false 153 | csharp_space_after_colon_in_inheritance_clause = true 154 | csharp_space_after_comma = true 155 | csharp_space_after_dot = false 156 | csharp_space_after_keywords_in_control_flow_statements = true 157 | csharp_space_after_semicolon_in_for_statement = true 158 | csharp_space_around_binary_operators = before_and_after 159 | csharp_space_around_declaration_statements = do_not_ignore 160 | csharp_space_before_colon_in_inheritance_clause = true 161 | csharp_space_before_comma = false 162 | csharp_space_before_dot = false 163 | csharp_space_before_open_square_brackets = false 164 | csharp_space_before_semicolon_in_for_statement = false 165 | csharp_space_between_empty_square_brackets = false 166 | csharp_space_between_method_call_empty_parameter_list_parentheses = false 167 | csharp_space_between_method_call_name_and_opening_parenthesis = false 168 | csharp_space_between_method_call_parameter_list_parentheses = false 169 | csharp_space_between_method_declaration_empty_parameter_list_parentheses = false 170 | csharp_space_between_method_declaration_name_and_open_parenthesis = false 171 | csharp_space_between_method_declaration_parameter_list_parentheses = false 172 | csharp_space_between_parentheses = false 173 | csharp_space_between_square_brackets = false 174 | 175 | # Blocks are allowed 176 | csharp_prefer_braces = true:silent 177 | csharp_preserve_single_line_blocks = true 178 | csharp_preserve_single_line_statements = true 179 | 180 | # warning RS0037: PublicAPI.txt is missing '#nullable enable' 181 | dotnet_diagnostic.RS0037.severity = none -------------------------------------------------------------------------------- /.github/funding.yml: -------------------------------------------------------------------------------- 1 | github: patriksvensson -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | # yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json 2 | name: Continuous Integration 3 | 4 | on: 5 | push: 6 | tags: 7 | - '*' 8 | branches: 9 | - main 10 | 11 | env: 12 | DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true 13 | DOTNET_CLI_TELEMETRY_OPTOUT: true 14 | 15 | jobs: 16 | 17 | ################################################### 18 | # PUBLISH 19 | ################################################### 20 | 21 | publish: 22 | name: Publish 23 | if: "!contains(github.event.head_commit.message, 'skip-ci')" 24 | runs-on: ubuntu-latest 25 | steps: 26 | - name: Checkout 27 | uses: actions/checkout@v4 28 | with: 29 | fetch-depth: 0 30 | 31 | - name: Setup dotnet 32 | uses: actions/setup-dotnet@v3 33 | 34 | - name: Publish 35 | shell: bash 36 | run: | 37 | dotnet tool restore 38 | dotnet cake --target="publish" \ 39 | --nuget-key="${{secrets.NUGET_API_KEY}}" 40 | -------------------------------------------------------------------------------- /.github/workflows/pr.yml: -------------------------------------------------------------------------------- 1 | # yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json 2 | name: Pull Request 3 | on: pull_request 4 | 5 | env: 6 | DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true 7 | DOTNET_CLI_TELEMETRY_OPTOUT: true 8 | 9 | jobs: 10 | 11 | ################################################### 12 | # BUILD 13 | ################################################### 14 | 15 | build: 16 | name: Build 17 | if: "!contains(github.event.head_commit.message, 'skip-ci')" 18 | runs-on: ubuntu-latest 19 | steps: 20 | - name: Checkout 21 | uses: actions/checkout@v4 22 | with: 23 | fetch-depth: 0 24 | 25 | - name: Setup .NET SDK 26 | uses: actions/setup-dotnet@v3 27 | 28 | - name: Build 29 | shell: bash 30 | run: | 31 | dotnet tool restore 32 | dotnet cake 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Misc folders 2 | [Bb]in/ 3 | [Oo]bj/ 4 | [Tt]emp/ 5 | [Pp]ackages/ 6 | 7 | # Build 8 | /.artifacts/ 9 | /[Tt]ools/ 10 | 11 | # Visual Studio 12 | .vs/ 13 | .vscode/ 14 | launchSettings.json 15 | *.sln.ide/ 16 | 17 | # Rider 18 | .idea/ 19 | 20 | ## Ignore Visual Studio temporary files, build results, and 21 | ## files generated by popular Visual Studio add-ons. 22 | 23 | # User-specific files 24 | *.suo 25 | *.user 26 | *.sln.docstates 27 | *.userprefs 28 | *.GhostDoc.xml 29 | *StyleCop.Cache 30 | 31 | # Build results 32 | [Dd]ebug/ 33 | [Rr]elease/ 34 | x64/ 35 | *_i.c 36 | *_p.c 37 | *.ilk 38 | *.meta 39 | *.obj 40 | *.pch 41 | *.pdb 42 | *.pgc 43 | *.pgd 44 | *.rsp 45 | *.sbr 46 | *.tlb 47 | *.tli 48 | *.tlh 49 | *.tmp 50 | *.log 51 | *.vspscc 52 | *.vssscc 53 | .builds 54 | 55 | # Visual Studio profiler 56 | *.psess 57 | *.vsp 58 | *.vspx 59 | 60 | # ReSharper is a .NET coding add-in 61 | _ReSharper* 62 | 63 | # NCrunch 64 | *.ncrunch* 65 | .*crunch*.local.xml 66 | _NCrunch_* 67 | 68 | # NuGet Packages Directory 69 | packages 70 | 71 | # Windows 72 | Thumbs.db -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at patrik@patriksvensson.se. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Patrik Svensson 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 | # Verify.Terminal 2 | 3 | A dotnet tool for managing Verify snapshots. 4 | Inspired by the awesome [Insta](https://github.com/mitsuhiko/insta) crate. 5 | 6 | ![A screenshot of Verify.Terminal](res/screenshot.png) 7 | 8 | ## Installation 9 | 10 | Install by running the following command: 11 | 12 | ```bash 13 | dotnet tool install -g verify.tool 14 | ``` 15 | 16 | ## Review pending snapshots 17 | 18 | ``` 19 | USAGE: 20 | verify review [OPTIONS] 21 | 22 | OPTIONS: 23 | -h, --help Prints help information 24 | -w, --work The working directory to use 25 | -c, --context The number of context lines to show. Defaults to 2 26 | ``` 27 | 28 | ``` 29 | > dotnet verify review 30 | ``` 31 | 32 | ## Accept all pending snapshots 33 | 34 | ``` 35 | USAGE: 36 | verify accept [OPTIONS] 37 | 38 | OPTIONS: 39 | -h, --help Prints help information 40 | -w, --work The working directory to use 41 | -y, --yes Confirm all prompts. 42 | ``` 43 | 44 | ``` 45 | > dotnet verify accept 46 | ``` 47 | 48 | ## Reject all pending snapshots 49 | 50 | ``` 51 | USAGE: 52 | verify reject [OPTIONS] 53 | 54 | OPTIONS: 55 | -h, --help Prints help information 56 | -w, --work The working directory to use 57 | -y, --yes Confirm all prompts. 58 | ``` 59 | 60 | ``` 61 | > dotnet verify reject 62 | ``` 63 | 64 | ## Building 65 | 66 | We're using [Cake](https://github.com/cake-build/cake) as a 67 | [dotnet tool](https://docs.microsoft.com/en-us/dotnet/core/tools/global-tools) 68 | for building. So make sure that you've restored Cake by running 69 | the following in the repository root: 70 | 71 | ``` 72 | > dotnet tool restore 73 | ``` 74 | 75 | After that, running the build is as easy as writing: 76 | 77 | ``` 78 | > dotnet cake 79 | ``` 80 | -------------------------------------------------------------------------------- /build.cake: -------------------------------------------------------------------------------- 1 | var target = Argument("target", "Default"); 2 | var configuration = Argument("configuration", "Release"); 3 | 4 | //////////////////////////////////////////////////////////////// 5 | // Tasks 6 | 7 | Task("Build") 8 | .Does(context => 9 | { 10 | DotNetBuild("./src/Verify.Terminal.sln", new DotNetBuildSettings { 11 | Configuration = configuration, 12 | NoIncremental = context.HasArgument("rebuild"), 13 | MSBuildSettings = new DotNetMSBuildSettings() 14 | .TreatAllWarningsAs(MSBuildTreatAllWarningsAs.Error) 15 | }); 16 | }); 17 | 18 | Task("Test") 19 | .IsDependentOn("Build") 20 | .Does(context => 21 | { 22 | DotNetTest("./src/Verify.Terminal.sln", new DotNetTestSettings { 23 | Configuration = configuration, 24 | NoRestore = true, 25 | NoBuild = true, 26 | }); 27 | }); 28 | 29 | Task("Pack") 30 | .IsDependentOn("Test") 31 | .Does(context => 32 | { 33 | CleanDirectory("./.artifacts"); 34 | 35 | DotNetPack("./src/Verify.Terminal.sln", new DotNetPackSettings { 36 | Configuration = configuration, 37 | NoRestore = true, 38 | NoBuild = true, 39 | OutputDirectory = "./.artifacts", 40 | MSBuildSettings = new DotNetMSBuildSettings() 41 | .TreatAllWarningsAs(MSBuildTreatAllWarningsAs.Error) 42 | .WithProperty("SymbolPackageFormat", "snupkg") 43 | }); 44 | }); 45 | 46 | Task("Publish") 47 | .WithCriteria(ctx => BuildSystem.IsRunningOnGitHubActions, "Not running on GitHub Actions") 48 | .IsDependentOn("Pack") 49 | .Does(context => 50 | { 51 | var apiKey = Argument("nuget-key", null); 52 | if(string.IsNullOrWhiteSpace(apiKey)) { 53 | throw new CakeException("No NuGet API key was provided."); 54 | } 55 | 56 | // Publish to GitHub Packages 57 | foreach(var file in context.GetFiles("./.artifacts/*.nupkg")) 58 | { 59 | Information("Publishing {0}...", file.GetFilename().FullPath); 60 | DotNetNuGetPush(file.FullPath, new DotNetNuGetPushSettings 61 | { 62 | Source = "https://api.nuget.org/v3/index.json", 63 | ApiKey = apiKey, 64 | }); 65 | } 66 | }); 67 | 68 | //////////////////////////////////////////////////////////////// 69 | // Targets 70 | 71 | Task("Default") 72 | .IsDependentOn("Pack"); 73 | 74 | //////////////////////////////////////////////////////////////// 75 | // Execution 76 | 77 | RunTarget(target) 78 | -------------------------------------------------------------------------------- /dotnet-tools.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "isRoot": true, 4 | "tools": { 5 | "cake.tool": { 6 | "version": "3.2.0", 7 | "commands": [ 8 | "dotnet-cake" 9 | ] 10 | }, 11 | "dotnet-example": { 12 | "version": "1.6.0", 13 | "commands": [ 14 | "dotnet-example" 15 | ] 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /global.json: -------------------------------------------------------------------------------- 1 | { 2 | "sdk": { 3 | "version": "8.0.203", 4 | "rollForward": "latestFeature" 5 | } 6 | } -------------------------------------------------------------------------------- /res/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VerifyTests/Verify.Terminal/a06204ee9ab30edee64c9d74e23958254c54ac22/res/screenshot.png -------------------------------------------------------------------------------- /src/.editorconfig: -------------------------------------------------------------------------------- 1 | root = false 2 | 3 | [*.cs] 4 | # IDE0055: Fix formatting 5 | dotnet_diagnostic.IDE0055.severity = warning 6 | 7 | # SA1101: Prefix local calls with this 8 | dotnet_diagnostic.SA1101.severity = none 9 | 10 | # SA1600: Elements should be documented 11 | dotnet_diagnostic.SA1600.severity = none 12 | 13 | # SA1633: File should have header 14 | dotnet_diagnostic.SA1633.severity = none 15 | 16 | # CS1591: Missing XML comment for publicly visible type or member 17 | dotnet_diagnostic.CS1591.severity = none 18 | 19 | # SA1200: Using directives should be placed correctly 20 | dotnet_diagnostic.SA1200.severity = none 21 | 22 | # SA1201: Elements should appear in the correct order 23 | dotnet_diagnostic.SA1201.severity = none 24 | 25 | # SA1202: Public members should come before private members 26 | dotnet_diagnostic.SA1202.severity = none 27 | 28 | # SA1309: Field names should not begin with underscore 29 | dotnet_diagnostic.SA1309.severity = none 30 | 31 | # SA1404: Code analysis suppressions should have justification 32 | dotnet_diagnostic.SA1404.severity = none 33 | 34 | # SA1516: Elements should be separated by a blank line 35 | dotnet_diagnostic.SA1516.severity = none 36 | 37 | # CA1303: Do not pass literals as localized parameters 38 | dotnet_diagnostic.CA1303.severity = none 39 | 40 | # CSA1204: Static members should appear before non-static members 41 | dotnet_diagnostic.SA1204.severity = none 42 | 43 | # IDE0052: Remove unread private members 44 | dotnet_diagnostic.IDE0052.severity = warning 45 | 46 | # IDE0063: Use simple 'using' statement 47 | csharp_prefer_simple_using_statement = false:suggestion 48 | 49 | # IDE0018: Variable declaration can be inlined 50 | dotnet_diagnostic.IDE0018.severity = warning 51 | 52 | # SA1625: Element documenation should not be copied and pasted 53 | dotnet_diagnostic.SA1625.severity = none 54 | 55 | # IDE0005: Using directive is unnecessary 56 | dotnet_diagnostic.IDE0005.severity = warning 57 | 58 | # SA1117: Parameters should be on same line or separate lines 59 | dotnet_diagnostic.SA1117.severity = none 60 | 61 | # SA1404: Code analysis suppression should have justification 62 | dotnet_diagnostic.SA1404.severity = none 63 | 64 | # SA1101: Prefix local calls with this 65 | dotnet_diagnostic.SA1101.severity = none 66 | 67 | # SA1633: File should have header 68 | dotnet_diagnostic.SA1633.severity = none 69 | 70 | # SA1649: File name should match first type name 71 | dotnet_diagnostic.SA1649.severity = none 72 | 73 | # SA1402: File may only contain a single type 74 | dotnet_diagnostic.SA1402.severity = none 75 | 76 | # CA1814: Prefer jagged arrays over multidimensional 77 | dotnet_diagnostic.CA1814.severity = none 78 | 79 | # RCS1194: Implement exception constructors. 80 | dotnet_diagnostic.RCS1194.severity = none 81 | 82 | # CA1032: Implement standard exception constructors 83 | dotnet_diagnostic.CA1032.severity = none 84 | 85 | # CA1826: Do not use Enumerable methods on indexable collections. Instead use the collection directly 86 | dotnet_diagnostic.CA1826.severity = none 87 | 88 | # RCS1079: Throwing of new NotImplementedException. 89 | dotnet_diagnostic.RCS1079.severity = warning 90 | 91 | # RCS1057: Add empty line between declarations. 92 | dotnet_diagnostic.RCS1057.severity = none 93 | 94 | # RCS1057: Validate arguments correctly 95 | dotnet_diagnostic.RCS1227.severity = none 96 | 97 | # IDE0004: Remove Unnecessary Cast 98 | dotnet_diagnostic.IDE0004.severity = warning 99 | 100 | # CA1810: Initialize reference type static fields inline 101 | dotnet_diagnostic.CA1810.severity = none 102 | 103 | # IDE0044: Add readonly modifier 104 | dotnet_diagnostic.IDE0044.severity = warning 105 | 106 | # RCS1047: Non-asynchronous method name should not end with 'Async'. 107 | dotnet_diagnostic.RCS1047.severity = none 108 | 109 | # RCS1090: Call 'ConfigureAwait(false)'. 110 | dotnet_diagnostic.RCS1090.severity = warning 111 | 112 | # SA1602: Enumeration items should be documented 113 | dotnet_diagnostic.SA1602.severity = none -------------------------------------------------------------------------------- /src/Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | true 4 | preview 5 | enable 6 | true 7 | embedded 8 | true 9 | true 10 | false 11 | true 12 | enable 13 | 14 | 15 | 16 | true 17 | true 18 | 19 | 20 | 21 | A dotnet tool for managing Verify snapshots. 22 | Copyright Patrik Svensson, Simon Cropp 23 | Patrik Svensson, Simon Cropp 24 | git 25 | https://github.com/patriksvensson/verify.terminal 26 | True 27 | https://github.com/patriksvensson/verify.terminal 28 | MIT 29 | https://github.com/patriksvensson/verify.terminal/releases 30 | true 31 | 32 | 33 | 34 | true 35 | true 36 | $(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb 37 | true 38 | 39 | 40 | 41 | 42 | 43 | All 44 | 45 | 46 | All 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /src/Directory.Build.targets: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | preview 5 | normal 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/Verify.Terminal.Tests/.editorconfig: -------------------------------------------------------------------------------- 1 | root = false 2 | [*.cs] 3 | 4 | # Default severity for analyzer diagnostics with category 'StyleCop.CSharp.DocumentationRules' 5 | dotnet_analyzer_diagnostic.category-StyleCop.CSharp.DocumentationRules.severity = none 6 | 7 | # CA1707: Identifiers should not contain underscores 8 | dotnet_diagnostic.CA1707.severity = none 9 | 10 | # SA1600: Elements should be documented 11 | dotnet_diagnostic.SA1600.severity = none 12 | 13 | # SA1601: Partial elements should be documented 14 | dotnet_diagnostic.SA1601.severity = none 15 | 16 | # SA1200: Using directives should be placed correctly 17 | dotnet_diagnostic.SA1200.severity = none 18 | 19 | # CS1591: Missing XML comment for publicly visible type or member 20 | dotnet_diagnostic.CS1591.severity = none 21 | 22 | # SA1210: Using directives should be ordered alphabetically by namespace 23 | dotnet_diagnostic.SA1210.severity = none 24 | 25 | # CA1034: Nested types should not be visible 26 | dotnet_diagnostic.CA1034.severity = none 27 | 28 | # CA2000: Dispose objects before losing scope 29 | dotnet_diagnostic.CA2000.severity = none 30 | 31 | # SA1118: Parameter should not span multiple lines 32 | dotnet_diagnostic.SA1118.severity = none 33 | 34 | # CA1031: Do not catch general exception types 35 | dotnet_diagnostic.CA1031.severity = none -------------------------------------------------------------------------------- /src/Verify.Terminal.Tests/Data/First/new: -------------------------------------------------------------------------------- 1 | Diagnostics { 2 | diagnostics: [ 3 | Diagnostic { 4 | code: "MEW1002", 5 | location: Location { 6 | path: "file.mew", 7 | span: Span { 8 | position: 0, 9 | length: 1, 10 | }, 11 | }, 12 | severity: Error, 13 | }, 14 | ], 15 | } 16 | -------------------------------------------------------------------------------- /src/Verify.Terminal.Tests/Data/First/old: -------------------------------------------------------------------------------- 1 | Diagnostics { 2 | diagnostics: [ 3 | Diagnostic { 4 | code: "MEW1001", 5 | location: Location { 6 | path: "file.mew", 7 | span: Span { 8 | position: 0, 9 | length: 1, 10 | }, 11 | }, 12 | message: "Unexpected character '='", 13 | severity: Verbose, 14 | }, 15 | ], 16 | } 17 | -------------------------------------------------------------------------------- /src/Verify.Terminal.Tests/Data/Second/new: -------------------------------------------------------------------------------- 1 | +-Greeting----+ 2 | | | 3 | | Hello World | 4 | | | 5 | +-------------+ -------------------------------------------------------------------------------- /src/Verify.Terminal.Tests/Data/Second/old: -------------------------------------------------------------------------------- 1 | +-Greeting----+ 2 | | Hello World | 3 | +-------------+ 4 | -------------------------------------------------------------------------------- /src/Verify.Terminal.Tests/Expectations/Rendering/Render.Output_First.verified.txt: -------------------------------------------------------------------------------- 1 | ──────────────────────────────────────────────────────────────────────────────── 2 | First.received.txt 3 | ──────────────────────────────────────────────────────────────────────────────── 4 | -old snapshot 5 | +new snapshot 6 | ─────────────┬─┬──────────────────────────────────────────────────────────────── 7 | 2 2 │ │····diagnostics:·[ 8 | 3 3 │ │········Diagnostic·{ 9 | 4 │-│············code:·"MEW1001", 10 | 4 │+│············code:·"MEW1002", 11 | 5 5 │ │············location:·Location·{ 12 | 6 6 │ │················path:·"file.mew", 13 | ─────────────┼─┼──────────────────────────────────────────────────────────────── 14 | 10 10 │ │················}, 15 | 11 11 │ │············}, 16 | 12 │-│············message:·"Unexpected·character·'='", 17 | 12 │+│············severity:·Error, 18 | 13 │-│············severity:·Verbose, 19 | 14 14 │ │········}, 20 | 15 15 │ │····], 21 | ─────────────┴─┴──────────────────────────────────────────────────────────────── -------------------------------------------------------------------------------- /src/Verify.Terminal.Tests/Expectations/Rendering/Render.Output_Second.verified.txt: -------------------------------------------------------------------------------- 1 | ──────────────────────────────────────────────────────────────────────────────── 2 | Second.received.txt 3 | ──────────────────────────────────────────────────────────────────────────────── 4 | -old snapshot 5 | +new snapshot 6 | ───────────┬─┬────────────────────────────────────────────────────────────────── 7 | 1 1 │ │+-Greeting----+ 8 | 2 │+│|·············| 9 | 3 3 │ │|·Hello·World·| 10 | 4 │+│|·············| 11 | 5 5 │ │+-------------+ 12 | ───────────┴─┴────────────────────────────────────────────────────────────────── -------------------------------------------------------------------------------- /src/Verify.Terminal.Tests/GlobalUsings.cs: -------------------------------------------------------------------------------- 1 | global using System.Reflection; 2 | global using System.Runtime.CompilerServices; 3 | global using Shouldly; 4 | global using Spectre.Console.Testing; 5 | global using Spectre.IO; 6 | global using Spectre.IO.Testing; -------------------------------------------------------------------------------- /src/Verify.Terminal.Tests/Properties/Usings.cs: -------------------------------------------------------------------------------- 1 | global using Xunit; -------------------------------------------------------------------------------- /src/Verify.Terminal.Tests/Properties/VerifyConfig.cs: -------------------------------------------------------------------------------- 1 | namespace Verify.Terminal.Tests; 2 | 3 | public static class VerifyConfiguration 4 | { 5 | [ModuleInitializer] 6 | public static void Init() 7 | { 8 | DerivePathInfo(Expectations.Initialize); 9 | } 10 | } -------------------------------------------------------------------------------- /src/Verify.Terminal.Tests/SnapshotFinderTests.cs: -------------------------------------------------------------------------------- 1 | namespace Verify.Terminal.Tests; 2 | 3 | public sealed class SnapshotFinderTests 4 | { 5 | [Fact] 6 | public void Should_Return_Expected_Snapshot() 7 | { 8 | // Given 9 | var environment = new FakeEnvironment(Spectre.IO.PlatformFamily.Linux); 10 | var filesystem = new FakeFileSystem(environment); 11 | var globber = new Globber(filesystem, environment); 12 | filesystem.CreateFile("/Working/lol.received.txt"); 13 | filesystem.CreateFile("/Working/lol.verified.txt"); 14 | var finder = new SnapshotFinder(filesystem, globber, environment); 15 | 16 | // When 17 | var result = finder.Find().SingleOrDefault(); 18 | 19 | // Then 20 | result.ShouldNotBeNull(); 21 | result.IsRerouted.ShouldBeFalse(); 22 | result.Received.FullPath.ShouldBe("/Working/lol.received.txt"); 23 | result.Verified.FullPath.ShouldBe("/Working/lol.verified.txt"); 24 | } 25 | 26 | [Fact] 27 | public void Should_Return_Expected_Snapshot_For_Non_Framework_Specific_File() 28 | { 29 | // Given 30 | var environment = new FakeEnvironment(Spectre.IO.PlatformFamily.Linux); 31 | var filesystem = new FakeFileSystem(environment); 32 | var globber = new Globber(filesystem, environment); 33 | filesystem.CreateFile("/Working/lol.DotNet6_0.received.txt"); 34 | filesystem.CreateFile("/Working/lol.verified.txt"); 35 | var finder = new SnapshotFinder(filesystem, globber, environment); 36 | 37 | // When 38 | var result = finder.Find().SingleOrDefault(); 39 | 40 | // Then 41 | result.ShouldNotBeNull(); 42 | result.IsRerouted.ShouldBeTrue(); 43 | result.Received.FullPath.ShouldBe("/Working/lol.DotNet6_0.received.txt"); 44 | result.Verified.FullPath.ShouldBe("/Working/lol.verified.txt"); 45 | } 46 | 47 | [Fact] 48 | public void Should_Return_Expected_Snapshot_For_Framework_Specific_File() 49 | { 50 | // Given 51 | var environment = new FakeEnvironment(Spectre.IO.PlatformFamily.Linux); 52 | var filesystem = new FakeFileSystem(environment); 53 | var globber = new Globber(filesystem, environment); 54 | filesystem.CreateFile("/Working/lol.DotNet6_0.received.txt"); 55 | filesystem.CreateFile("/Working/lol.DotNet6_0.verified.txt"); 56 | var finder = new SnapshotFinder(filesystem, globber, environment); 57 | 58 | // When 59 | var result = finder.Find().SingleOrDefault(); 60 | 61 | // Then 62 | result.ShouldNotBeNull(); 63 | result.IsRerouted.ShouldBeFalse(); 64 | result.Received.FullPath.ShouldBe("/Working/lol.DotNet6_0.received.txt"); 65 | result.Verified.FullPath.ShouldBe("/Working/lol.DotNet6_0.verified.txt"); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/Verify.Terminal.Tests/SnapshotRendererTests.cs: -------------------------------------------------------------------------------- 1 | namespace Verify.Terminal.Tests; 2 | 3 | [UsesVerify] 4 | [ExpectationPath("Rendering")] 5 | public class SnapshotRendererTests 6 | { 7 | [Theory] 8 | [Expectation("Render")] 9 | [InlineData("First")] 10 | [InlineData("Second")] 11 | public Task Should_Render_Correctly(string scenario) 12 | { 13 | // Given 14 | var environment = new FakeEnvironment(PlatformFamily.Linux); 15 | var filesystem = new FakeFileSystem(environment); 16 | var console = new TestConsole(); 17 | var renderer = new SnapshotRenderer(console); 18 | var differ = new SnapshotDiffer(filesystem, environment); 19 | 20 | filesystem.CreateFile($"/input/{scenario}.verified.txt") 21 | .SetEmbedded($"Verify.Terminal.Tests/Data/{scenario}/old"); 22 | filesystem.CreateFile($"/input/{scenario}.received.txt") 23 | .SetEmbedded($"Verify.Terminal.Tests/Data/{scenario}/new"); 24 | 25 | var diff = differ.Diff( 26 | new Snapshot($"/input/{scenario}.received.txt")); 27 | 28 | // When 29 | console.Write(renderer.Render(diff, contextLines: 2)); 30 | 31 | // Then 32 | return Verifier.Verify(console.Output) 33 | .UseTextForParameters(scenario); 34 | } 35 | } -------------------------------------------------------------------------------- /src/Verify.Terminal.Tests/Utilities/EmbeddedResourceReader.cs: -------------------------------------------------------------------------------- 1 | namespace Verify.Terminal.Tests; 2 | 3 | public static class EmbeddedResourceReader 4 | { 5 | public static string LoadResourceStream(string resourceName) 6 | { 7 | if (resourceName is null) 8 | { 9 | throw new ArgumentNullException(nameof(resourceName)); 10 | } 11 | 12 | var assembly = Assembly.GetCallingAssembly(); 13 | resourceName = resourceName.Replace("/", "."); 14 | 15 | using var stream = assembly.GetManifestResourceStream(resourceName); 16 | using var reader = new StreamReader(stream); 17 | return reader.ReadToEnd(); 18 | } 19 | } -------------------------------------------------------------------------------- /src/Verify.Terminal.Tests/Utilities/FileSystemExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace Verify.Terminal.Tests; 2 | 3 | internal static class FileSystemExtensions 4 | { 5 | public static FakeFile SetEmbedded(this FakeFile file, string path) 6 | { 7 | file.SetTextContent(EmbeddedResourceReader.LoadResourceStream(path)); 8 | return file; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/Verify.Terminal.Tests/Verify.Terminal.Tests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net8.0 5 | disable 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 | all 35 | runtime; build; native; contentfiles; analyzers; buildtransitive 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /src/Verify.Terminal.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.0.31903.59 5 | MinimumVisualStudioVersion = 15.0.26124.0 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Verify.Terminal", "Verify.Terminal\Verify.Terminal.csproj", "{97277350-2069-4863-B0DE-07E929629958}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Verify.Terminal.Tests", "Verify.Terminal.Tests\Verify.Terminal.Tests.csproj", "{A90C8C46-58DF-41B8-B273-B24396E8424C}" 9 | EndProject 10 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Build", "Build", "{87E8CA05-E850-41BB-B5D5-6863848F223F}" 11 | ProjectSection(SolutionItems) = preProject 12 | Directory.Build.props = Directory.Build.props 13 | Directory.Build.targets = Directory.Build.targets 14 | EndProjectSection 15 | EndProject 16 | Global 17 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 18 | Debug|Any CPU = Debug|Any CPU 19 | Debug|x64 = Debug|x64 20 | Debug|x86 = Debug|x86 21 | Release|Any CPU = Release|Any CPU 22 | Release|x64 = Release|x64 23 | Release|x86 = Release|x86 24 | EndGlobalSection 25 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 26 | {97277350-2069-4863-B0DE-07E929629958}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 27 | {97277350-2069-4863-B0DE-07E929629958}.Debug|Any CPU.Build.0 = Debug|Any CPU 28 | {97277350-2069-4863-B0DE-07E929629958}.Debug|x64.ActiveCfg = Debug|Any CPU 29 | {97277350-2069-4863-B0DE-07E929629958}.Debug|x64.Build.0 = Debug|Any CPU 30 | {97277350-2069-4863-B0DE-07E929629958}.Debug|x86.ActiveCfg = Debug|Any CPU 31 | {97277350-2069-4863-B0DE-07E929629958}.Debug|x86.Build.0 = Debug|Any CPU 32 | {97277350-2069-4863-B0DE-07E929629958}.Release|Any CPU.ActiveCfg = Release|Any CPU 33 | {97277350-2069-4863-B0DE-07E929629958}.Release|Any CPU.Build.0 = Release|Any CPU 34 | {97277350-2069-4863-B0DE-07E929629958}.Release|x64.ActiveCfg = Release|Any CPU 35 | {97277350-2069-4863-B0DE-07E929629958}.Release|x64.Build.0 = Release|Any CPU 36 | {97277350-2069-4863-B0DE-07E929629958}.Release|x86.ActiveCfg = Release|Any CPU 37 | {97277350-2069-4863-B0DE-07E929629958}.Release|x86.Build.0 = Release|Any CPU 38 | {A90C8C46-58DF-41B8-B273-B24396E8424C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 39 | {A90C8C46-58DF-41B8-B273-B24396E8424C}.Debug|Any CPU.Build.0 = Debug|Any CPU 40 | {A90C8C46-58DF-41B8-B273-B24396E8424C}.Debug|x64.ActiveCfg = Debug|Any CPU 41 | {A90C8C46-58DF-41B8-B273-B24396E8424C}.Debug|x64.Build.0 = Debug|Any CPU 42 | {A90C8C46-58DF-41B8-B273-B24396E8424C}.Debug|x86.ActiveCfg = Debug|Any CPU 43 | {A90C8C46-58DF-41B8-B273-B24396E8424C}.Debug|x86.Build.0 = Debug|Any CPU 44 | {A90C8C46-58DF-41B8-B273-B24396E8424C}.Release|Any CPU.ActiveCfg = Release|Any CPU 45 | {A90C8C46-58DF-41B8-B273-B24396E8424C}.Release|Any CPU.Build.0 = Release|Any CPU 46 | {A90C8C46-58DF-41B8-B273-B24396E8424C}.Release|x64.ActiveCfg = Release|Any CPU 47 | {A90C8C46-58DF-41B8-B273-B24396E8424C}.Release|x64.Build.0 = Release|Any CPU 48 | {A90C8C46-58DF-41B8-B273-B24396E8424C}.Release|x86.ActiveCfg = Release|Any CPU 49 | {A90C8C46-58DF-41B8-B273-B24396E8424C}.Release|x86.Build.0 = Release|Any CPU 50 | EndGlobalSection 51 | GlobalSection(SolutionProperties) = preSolution 52 | HideSolutionNode = FALSE 53 | EndGlobalSection 54 | GlobalSection(ExtensibilityGlobals) = postSolution 55 | SolutionGuid = {91DC3E44-F01D-459A-A956-418FE23E6723} 56 | EndGlobalSection 57 | EndGlobal 58 | -------------------------------------------------------------------------------- /src/Verify.Terminal.sln.DotSettings: -------------------------------------------------------------------------------- 1 |  2 | DO_NOT_SHOW -------------------------------------------------------------------------------- /src/Verify.Terminal/Commands/Modify/AcceptCommand.cs: -------------------------------------------------------------------------------- 1 | namespace Verify.Terminal.Commands; 2 | 3 | public sealed class AcceptCommand : ModifyCommand 4 | { 5 | public override string Verb { get; } = "Accept"; 6 | public override SnapshotAction Action { get; } = SnapshotAction.Accept; 7 | 8 | public AcceptCommand( 9 | SnapshotFinder snapshotFinder, 10 | SnapshotManager snapshotManager) 11 | : base(snapshotFinder, snapshotManager) 12 | { 13 | } 14 | } -------------------------------------------------------------------------------- /src/Verify.Terminal/Commands/Modify/RejectCommand.cs: -------------------------------------------------------------------------------- 1 | namespace Verify.Terminal.Commands; 2 | 3 | public sealed class RejectCommand : ModifyCommand 4 | { 5 | public override string Verb { get; } = "Reject"; 6 | public override SnapshotAction Action { get; } = SnapshotAction.Reject; 7 | 8 | public RejectCommand( 9 | SnapshotFinder snapshotFinder, 10 | SnapshotManager snapshotManager) 11 | : base(snapshotFinder, snapshotManager) 12 | { 13 | } 14 | } -------------------------------------------------------------------------------- /src/Verify.Terminal/Commands/ModifyCommand.cs: -------------------------------------------------------------------------------- 1 | namespace Verify.Terminal.Commands; 2 | 3 | public abstract class ModifyCommand : Command 4 | { 5 | private readonly SnapshotFinder _snapshotFinder; 6 | private readonly SnapshotManager _snapshotManager; 7 | 8 | public abstract string Verb { get; } 9 | public abstract SnapshotAction Action { get; } 10 | 11 | public sealed class Settings : CommandSettings 12 | { 13 | [CommandOption("-w|--work ")] 14 | [TypeConverter(typeof(DirectoryPathConverter))] 15 | [Description("The working directory to use")] 16 | public DirectoryPath? Root { get; set; } 17 | 18 | [CommandOption("-y|--yes")] 19 | [Description("Confirm all prompts. Chooses affirmative answer instead of prompting.")] 20 | public bool NoPrompt { get; set; } 21 | } 22 | 23 | protected ModifyCommand(SnapshotFinder snapshotFinder, SnapshotManager snapshotManager) 24 | { 25 | _snapshotFinder = snapshotFinder.NotNull(); 26 | _snapshotManager = snapshotManager.NotNull(); 27 | } 28 | 29 | public sealed override int Execute( 30 | [NotNull] CommandContext context, 31 | [NotNull] Settings settings) 32 | { 33 | // Get all snapshots and show a summary 34 | var snapshots = _snapshotFinder.Find(settings.Root); 35 | if (snapshots.Count == 0) 36 | { 37 | AnsiConsole.MarkupLine("[yellow]No snapshots found.[/]"); 38 | return 0; 39 | } 40 | 41 | // Proceed? 42 | AnsiConsole.Console.ShowSnapshotSummary(snapshots); 43 | if (!Proceed(settings, $"[yellow]{Verb} {snapshots.Count} snapshot(s)?[/]")) 44 | { 45 | return 1; 46 | } 47 | 48 | // Process snapshots 49 | foreach (var snapshot in snapshots) 50 | { 51 | if (!_snapshotManager.Process(snapshot, Action)) 52 | { 53 | AnsiConsole.MarkupLineInterpolated( 54 | $"[red]Error:[/] An error occured while processing snapshot: {snapshot.Received}"); 55 | return 2; 56 | } 57 | } 58 | 59 | return 0; 60 | } 61 | 62 | private static bool Proceed(Settings settings, string question) 63 | { 64 | if (settings.NoPrompt) 65 | { 66 | return true; 67 | } 68 | 69 | return AnsiConsole.Console.AskYesNo(question); 70 | } 71 | } -------------------------------------------------------------------------------- /src/Verify.Terminal/Commands/ReviewCommand.cs: -------------------------------------------------------------------------------- 1 | namespace Verify.Terminal.Commands; 2 | 3 | public sealed class ReviewCommand : Command 4 | { 5 | private readonly SnapshotFinder _snapshotFinder; 6 | private readonly SnapshotDiffer _snapshotDiffer; 7 | private readonly SnapshotManager _snapshotManager; 8 | private readonly SnapshotRenderer _snapshotRenderer; 9 | 10 | public ReviewCommand( 11 | SnapshotFinder snapshotFinder, 12 | SnapshotDiffer snapshotDiffer, 13 | SnapshotManager snapshotManager, 14 | SnapshotRenderer snapshotRenderer) 15 | { 16 | _snapshotFinder = snapshotFinder.NotNull(); 17 | _snapshotDiffer = snapshotDiffer.NotNull(); 18 | _snapshotManager = snapshotManager.NotNull(); 19 | _snapshotRenderer = snapshotRenderer.NotNull(); 20 | } 21 | 22 | public sealed class Settings : CommandSettings 23 | { 24 | [CommandOption("-w|--work ")] 25 | [TypeConverter(typeof(DirectoryPathConverter))] 26 | [Description("The working directory to use")] 27 | public DirectoryPath? Root { get; set; } 28 | 29 | [CommandOption("-c|--context ")] 30 | [Description("The number of context lines to show. Defaults to 2.")] 31 | public int ContextLines { get; set; } = 2; 32 | } 33 | 34 | public override int Execute( 35 | [NotNull] CommandContext context, 36 | [NotNull] Settings settings) 37 | { 38 | // Get all snapshots and show a summary 39 | var snapshots = _snapshotFinder.Find(settings.Root); 40 | if (snapshots.Count == 0) 41 | { 42 | AnsiConsole.MarkupLine("[yellow]No snapshots to review.[/]"); 43 | return 0; 44 | } 45 | 46 | foreach (var (index, first, last, snapshot) in snapshots.Enumerate()) 47 | { 48 | var diff = _snapshotDiffer.Diff(snapshot); 49 | 50 | if (!first) 51 | { 52 | AnsiConsole.WriteLine(); 53 | } 54 | 55 | AnsiConsole.WriteLine(); 56 | AnsiConsole.MarkupLine($"[yellow b]Reviewing[/] [[{index + 1}/{snapshots.Count}]]"); 57 | AnsiConsole.Write(_snapshotRenderer.Render(diff, Math.Max(0, settings.ContextLines))); 58 | 59 | switch (ShowPrompt()) 60 | { 61 | case SnapshotAction.Accept: 62 | _snapshotManager.Accept(snapshot); 63 | break; 64 | case SnapshotAction.Reject: 65 | _snapshotManager.Reject(snapshot); 66 | break; 67 | case SnapshotAction.Skip: 68 | continue; 69 | } 70 | 71 | if (!last) 72 | { 73 | AnsiConsole.WriteLine(); 74 | } 75 | } 76 | 77 | return 0; 78 | } 79 | 80 | private static SnapshotAction ShowPrompt() 81 | { 82 | var grid = new Grid(); 83 | grid.AddColumn(new GridColumn().PadLeft(4)); 84 | grid.AddColumn(new GridColumn().PadLeft(4)); 85 | grid.AddRow("[[[green]a[/]]]ccept", "[grey]keep the new snapshot[/]"); 86 | grid.AddRow("[[[red]r[/]]]eject", "[grey]keep the old snapshot[/]"); 87 | grid.AddRow("[[[yellow]s[/]]]kip", "[grey]keep both for now[/]"); 88 | AnsiConsole.WriteLine(); 89 | AnsiConsole.WriteLine(); 90 | AnsiConsole.Write(grid); 91 | 92 | try 93 | { 94 | AnsiConsole.Cursor.Hide(); 95 | 96 | while (true) 97 | { 98 | var key = Console.ReadKey(true); 99 | var action = key.Key switch 100 | { 101 | ConsoleKey.A => SnapshotAction.Accept, 102 | ConsoleKey.R => SnapshotAction.Reject, 103 | ConsoleKey.S => SnapshotAction.Skip, 104 | _ => (SnapshotAction?)null, 105 | }; 106 | 107 | if (action != null) 108 | { 109 | return action.Value; 110 | } 111 | } 112 | } 113 | finally 114 | { 115 | AnsiConsole.Cursor.Show(); 116 | } 117 | } 118 | } -------------------------------------------------------------------------------- /src/Verify.Terminal/Extensions/ConsoleExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace Verify.Terminal; 2 | 3 | public static class ConsoleExtensions 4 | { 5 | public static void ShowSnapshotSummary(this IAnsiConsole console, IEnumerable snapshots) 6 | { 7 | var table = new Table(); 8 | 9 | table.AddColumn("[blue]Snapshots[/]"); 10 | foreach (var snapshot in snapshots) 11 | { 12 | table.AddRow( 13 | new TextPath(snapshot.Received.FullPath) 14 | .LeafColor(Color.Blue)); 15 | } 16 | 17 | console.Write(table); 18 | } 19 | 20 | public static bool AskYesNo(this IAnsiConsole console, string question) 21 | { 22 | return console.Prompt(new SelectionPrompt() 23 | .Title(question) 24 | .AddChoices(true, false) 25 | .UseConverter(b => b ? "Yes" : "No")); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Verify.Terminal/Extensions/EnumerableExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace Verify.Terminal; 2 | 3 | internal static class EnumerableExtensions 4 | { 5 | public static IEnumerable<(int Index, bool First, bool Last, T Item)> Enumerate(this IEnumerable source) 6 | { 7 | return Enumerate(source.NotNull().GetEnumerator()); 8 | } 9 | 10 | public static IEnumerable<(int Index, bool First, bool Last, T Item)> Enumerate(this IEnumerator source) 11 | { 12 | source.NotNull(); 13 | 14 | var first = true; 15 | var last = !source.MoveNext(); 16 | T current; 17 | 18 | for (var index = 0; !last; index++) 19 | { 20 | current = source.Current; 21 | last = !source.MoveNext(); 22 | yield return (index, first, last, current); 23 | first = false; 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Verify.Terminal/Extensions/FilePathExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace Verify.Terminal; 2 | 3 | public static class FilePathExtensions 4 | { 5 | public static FilePath AppendExtensionIfNotNull(this FilePath path, string? extension) 6 | { 7 | if (string.IsNullOrWhiteSpace(extension)) 8 | { 9 | return path; 10 | } 11 | 12 | return path.AppendExtension(extension); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/Verify.Terminal/GlobalUsings.cs: -------------------------------------------------------------------------------- 1 | global using System.ComponentModel; 2 | global using System.Globalization; 3 | global using System.Runtime.CompilerServices; 4 | global using DiffPlex.DiffBuilder; 5 | global using DiffPlex.DiffBuilder.Model; 6 | global using Spectre.IO; -------------------------------------------------------------------------------- /src/Verify.Terminal/Guard.cs: -------------------------------------------------------------------------------- 1 | internal static class Guard 2 | { 3 | [return: NotNull] 4 | public static T NotNull([NotNull] this T? value, [CallerArgumentExpression("value")] string name = "") 5 | where T : class 6 | { 7 | if (value is null) 8 | { 9 | throw new ArgumentNullException(name); 10 | } 11 | 12 | return value; 13 | } 14 | } -------------------------------------------------------------------------------- /src/Verify.Terminal/Infrastructure/DirectoryPathConverter.cs: -------------------------------------------------------------------------------- 1 | namespace Verify.Terminal.Infrastructure; 2 | 3 | public sealed class DirectoryPathConverter : TypeConverter 4 | { 5 | public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType) 6 | { 7 | return sourceType == typeof(string) || base.CanConvertFrom(context, sourceType); 8 | } 9 | 10 | public override object? ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value) 11 | { 12 | if (value is string stringValue) 13 | { 14 | return new DirectoryPath(stringValue); 15 | } 16 | 17 | return base.ConvertFrom(context, culture, value); 18 | } 19 | 20 | public override bool CanConvertTo(ITypeDescriptorContext? context, Type? destinationType) 21 | { 22 | return destinationType == typeof(DirectoryPath) || base.CanConvertTo(context, destinationType); 23 | } 24 | 25 | public override object? ConvertTo(ITypeDescriptorContext? context, CultureInfo? culture, object? value, Type destinationType) 26 | { 27 | if (destinationType == typeof(string) && value is DirectoryPath filePathValue) 28 | { 29 | return filePathValue.FullPath; 30 | } 31 | 32 | return base.ConvertTo(context, culture, value, destinationType); 33 | } 34 | } -------------------------------------------------------------------------------- /src/Verify.Terminal/Infrastructure/TypeRegistrar.cs: -------------------------------------------------------------------------------- 1 | namespace Verify.Terminal.Infrastructure; 2 | 3 | public sealed class TypeRegistrar : ITypeRegistrar 4 | { 5 | private readonly IServiceCollection _services; 6 | 7 | public TypeRegistrar(IServiceCollection services) 8 | { 9 | _services = services.NotNull(); 10 | } 11 | 12 | public void Register(Type service, Type implementation) 13 | { 14 | _services.AddSingleton(service, implementation); 15 | } 16 | 17 | public void RegisterInstance(Type service, object implementation) 18 | { 19 | _services.AddSingleton(service, implementation); 20 | } 21 | 22 | public void RegisterLazy(Type service, Func factory) 23 | { 24 | _services.AddSingleton(service, _ => factory()); 25 | } 26 | 27 | public ITypeResolver Build() 28 | { 29 | return new TypeResolver(_services.BuildServiceProvider()); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Verify.Terminal/Infrastructure/TypeResolver.cs: -------------------------------------------------------------------------------- 1 | namespace Verify.Terminal.Infrastructure; 2 | 3 | public sealed class TypeResolver : ITypeResolver 4 | { 5 | private readonly IServiceProvider _provider; 6 | 7 | public TypeResolver(IServiceProvider provider) 8 | { 9 | _provider = provider; 10 | } 11 | 12 | public object? Resolve(Type? type) 13 | { 14 | return _provider.GetService(type.NotNull()); 15 | } 16 | } -------------------------------------------------------------------------------- /src/Verify.Terminal/Program.cs: -------------------------------------------------------------------------------- 1 | namespace Verify.Terminal; 2 | 3 | public static class Program 4 | { 5 | public static int Main(string[] args) 6 | { 7 | var container = BuildContainer(); 8 | 9 | var app = new CommandApp(container); 10 | app.Configure(config => 11 | { 12 | config.AddCommand("accept"); 13 | config.AddCommand("reject"); 14 | config.AddCommand("review"); 15 | 16 | config.SetApplicationName("verifyterm"); 17 | config.UseStrictParsing(); 18 | }); 19 | 20 | return app.Run(args); 21 | } 22 | 23 | private static TypeRegistrar BuildContainer() 24 | { 25 | var services = new ServiceCollection(); 26 | 27 | services.AddSingleton(); 28 | services.AddSingleton(); 29 | services.AddSingleton(); 30 | 31 | services.AddSingleton(); 32 | services.AddSingleton(); 33 | services.AddSingleton(); 34 | services.AddSingleton(); 35 | 36 | return new TypeRegistrar(services); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Verify.Terminal/Properties/Usings.cs: -------------------------------------------------------------------------------- 1 | global using System; 2 | global using System.Diagnostics.CodeAnalysis; 3 | global using Microsoft.Extensions.DependencyInjection; 4 | global using Spectre.Console; 5 | global using Spectre.Console.Cli; 6 | global using Verify.Terminal.Commands; 7 | global using Verify.Terminal.Infrastructure; 8 | -------------------------------------------------------------------------------- /src/Verify.Terminal/Rendering/Character.cs: -------------------------------------------------------------------------------- 1 | namespace Verify.Terminal.Rendering; 2 | 3 | public enum Character 4 | { 5 | /// 6 | /// Represents `:`. 7 | /// 8 | Colon, 9 | 10 | /// 11 | /// Represents `┌`. 12 | /// 13 | TopLeftCornerHard, 14 | 15 | /// 16 | /// Represents `└`. 17 | /// 18 | BottomLeftCornerHard, 19 | 20 | /// 21 | /// Represents `├`. 22 | /// 23 | LeftConnector, 24 | 25 | /// 26 | /// Represents `─`. 27 | /// 28 | HorizontalLine, 29 | 30 | /// 31 | /// Represents `│`. 32 | /// 33 | VerticalLine, 34 | 35 | /// 36 | /// Represents `·`. 37 | /// 38 | Dot, 39 | 40 | /// 41 | /// Represents `┬`. 42 | /// 43 | Anchor, 44 | 45 | /// 46 | /// Represents `─`. 47 | /// 48 | AnchorHorizontalLine, 49 | 50 | /// 51 | /// Represents `│`. 52 | /// 53 | AnchorVerticalLine, 54 | 55 | /// 56 | /// Represents `╰`. 57 | /// 58 | BottomLeftCornerRound, 59 | } 60 | -------------------------------------------------------------------------------- /src/Verify.Terminal/Rendering/CharacterSet.cs: -------------------------------------------------------------------------------- 1 | namespace Verify.Terminal.Rendering; 2 | 3 | /// 4 | /// Represents a character set. 5 | /// 6 | public abstract class CharacterSet 7 | { 8 | /// 9 | /// Gets a renderable representation of `:`. 10 | /// 11 | public virtual char Colon { get; } = ':'; 12 | 13 | /// 14 | /// Gets a renderable representation of `┌`. 15 | /// 16 | public virtual char TopLeftCornerHard { get; } = '┌'; 17 | 18 | /// 19 | /// Gets a renderable representation of `└`. 20 | /// 21 | public virtual char BottomLeftCornerHard { get; } = '└'; 22 | 23 | /// 24 | /// Gets a renderable representation of `├`. 25 | /// 26 | public virtual char LeftConnector { get; } = '├'; 27 | 28 | /// 29 | /// Gets a renderable representation of `─`. 30 | /// 31 | public virtual char HorizontalLine { get; } = '─'; 32 | 33 | /// 34 | /// Gets a renderable representation of `│`. 35 | /// 36 | public virtual char VerticalLine { get; } = '│'; 37 | 38 | /// 39 | /// Gets a renderable representation of `·`. 40 | /// 41 | public virtual char Dot { get; } = '·'; 42 | 43 | /// 44 | /// Gets a renderable representation of `┬`. 45 | /// 46 | public virtual char Anchor { get; } = '┬'; 47 | 48 | /// 49 | /// Gets a renderable representation of `─`. 50 | /// 51 | public virtual char AnchorHorizontalLine { get; } = '─'; 52 | 53 | /// 54 | /// Gets a renderable representation of `│`. 55 | /// 56 | public virtual char AnchorVerticalLine { get; } = '│'; 57 | 58 | /// 59 | /// Gets a renderable representation of `╰`. 60 | /// 61 | public virtual char BottomLeftCornerRound { get; } = '╰'; 62 | 63 | /// 64 | /// Gets a Unicode compatible character set. 65 | /// 66 | public static UnicodeCharacterSet Unicode => UnicodeCharacterSet.Shared; 67 | 68 | /// 69 | /// Gets an ASCII compatible character set. 70 | /// 71 | public static AsciiCharacterSet Ascii => AsciiCharacterSet.Shared; 72 | 73 | /// 74 | /// Gets a renderable representation of a . 75 | /// 76 | /// The character to get a renderable representation of. 77 | /// A renderable representation of a . 78 | public char Get(Character character) 79 | { 80 | return character switch 81 | { 82 | Character.Colon => Colon, 83 | Character.TopLeftCornerHard => TopLeftCornerHard, 84 | Character.BottomLeftCornerHard => BottomLeftCornerHard, 85 | Character.LeftConnector => LeftConnector, 86 | Character.HorizontalLine => HorizontalLine, 87 | Character.VerticalLine => VerticalLine, 88 | Character.Dot => Dot, 89 | Character.Anchor => Anchor, 90 | Character.AnchorHorizontalLine => AnchorHorizontalLine, 91 | Character.AnchorVerticalLine => AnchorVerticalLine, 92 | Character.BottomLeftCornerRound => BottomLeftCornerRound, 93 | _ => throw new NotSupportedException($"Unknown character '{character}'"), 94 | }; 95 | } 96 | 97 | /// 98 | /// Creates a that is compatible with the specified . 99 | /// 100 | /// The console. 101 | /// A that is compatible with the specified . 102 | public static CharacterSet Create(IAnsiConsole console) 103 | { 104 | return console.NotNull().Profile.Capabilities.Unicode 105 | ? UnicodeCharacterSet.Shared 106 | : AsciiCharacterSet.Shared; 107 | } 108 | } 109 | 110 | /// 111 | /// Represents a Unicode compatible character set. 112 | /// 113 | public class UnicodeCharacterSet : CharacterSet 114 | { 115 | internal static UnicodeCharacterSet Shared { get; } = new(); 116 | } 117 | 118 | /// 119 | /// Represents an ASCII compatible character set. 120 | /// 121 | public class AsciiCharacterSet : CharacterSet 122 | { 123 | internal static AsciiCharacterSet Shared { get; } = new(); 124 | 125 | /// 126 | public override char Anchor { get; } = '┬'; 127 | 128 | /// 129 | public override char AnchorHorizontalLine { get; } = '─'; 130 | 131 | /// 132 | public override char BottomLeftCornerRound { get; } = '└'; 133 | } -------------------------------------------------------------------------------- /src/Verify.Terminal/Rendering/ReportBuilder.cs: -------------------------------------------------------------------------------- 1 | using Spectre.Console.Rendering; 2 | 3 | namespace Verify.Terminal.Rendering; 4 | 5 | public sealed class ReportBuilder 6 | { 7 | private readonly List _buffer; 8 | private readonly List _lines; 9 | private readonly IAnsiConsole _console; 10 | 11 | public CharacterSet Characters { get; } 12 | 13 | public ReportBuilder(IAnsiConsole console, CharacterSet characters) 14 | { 15 | _console = console.NotNull(); 16 | _buffer = new List(); 17 | _lines = new List(); 18 | 19 | Characters = characters.NotNull(); 20 | } 21 | 22 | public void AppendInlineRenderable(IRenderable renderable) 23 | { 24 | var segments = renderable.GetSegments(_console).Where(s => !s.IsLineBreak); 25 | foreach (var segment in segments) 26 | { 27 | _buffer.Add(segment.StripLineEndings()); 28 | } 29 | } 30 | 31 | public void Append(Character character, Color? color = null, Decoration? decoration = null) 32 | { 33 | Append(Characters.Get(character), color, decoration); 34 | } 35 | 36 | public void Append(string text, Color? color = null, Decoration? decoration = null, int? maxLength = null) 37 | { 38 | text.NotNull(); 39 | 40 | if (maxLength > 0 && (text.Length - 1) > maxLength) 41 | { 42 | text = text[..maxLength.Value]; 43 | } 44 | 45 | _buffer.Add(new Segment(text, new Style(foreground: color, decoration: decoration))); 46 | } 47 | 48 | public void Append(char character, Color? color = null, Decoration? decoration = null) 49 | { 50 | _buffer.Add(new Segment(new string(character, 1), new Style(foreground: color, decoration: decoration))); 51 | } 52 | 53 | public void AppendRepeated(Character character, int count, Color? color = null) 54 | { 55 | AppendRepeated(Characters.Get(character), count, color); 56 | } 57 | 58 | public void AppendRepeated(char character, int count, Color? color = null) 59 | { 60 | Append(new string(character, count), color); 61 | } 62 | 63 | public void AppendSpace() 64 | { 65 | Append(" "); 66 | } 67 | 68 | public void AppendSpaces(int count) 69 | { 70 | if (count > 0) 71 | { 72 | Append(new string(' ', count)); 73 | } 74 | } 75 | 76 | public void CommitLine() 77 | { 78 | if (_buffer.Count == 0) 79 | { 80 | AppendSpace(); 81 | } 82 | 83 | _lines.Add(new SegmentLine(_buffer)); 84 | _buffer.Clear(); 85 | } 86 | 87 | public IReadOnlyList GetLines() 88 | { 89 | return _lines; 90 | } 91 | 92 | public IRenderable Build() 93 | { 94 | return new ReportRenderable(_lines); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/Verify.Terminal/Rendering/ReportRenderable.cs: -------------------------------------------------------------------------------- 1 | using Spectre.Console.Rendering; 2 | 3 | namespace Verify.Terminal.Rendering; 4 | 5 | internal sealed class ReportRenderable : IRenderable 6 | { 7 | private readonly List _lines; 8 | 9 | public ReportRenderable(IEnumerable lines) 10 | { 11 | _lines = new List(lines.NotNull()); 12 | } 13 | 14 | public Measurement Measure(RenderOptions context, int maxWidth) 15 | { 16 | return new Measurement(maxWidth, maxWidth); 17 | } 18 | 19 | public IEnumerable Render(RenderOptions context, int maxWidth) 20 | { 21 | return new SegmentLineEnumerator(_lines); 22 | } 23 | } -------------------------------------------------------------------------------- /src/Verify.Terminal/Snapshot.cs: -------------------------------------------------------------------------------- 1 | namespace Verify.Terminal; 2 | 3 | public sealed class Snapshot 4 | { 5 | public FilePath Received { get; } 6 | public FilePath Verified { get; } 7 | public bool IsRerouted { get; } 8 | 9 | public Snapshot(FilePath received) 10 | { 11 | Received = received.NotNull(); 12 | Verified = GetVerified(Received); 13 | } 14 | 15 | public Snapshot(FilePath received, FilePath verified, bool isRerouted) 16 | { 17 | Received = received.NotNull(); 18 | Verified = verified.NotNull(); 19 | IsRerouted = isRerouted; 20 | } 21 | 22 | private static FilePath GetVerified(FilePath received) 23 | { 24 | static FilePath StripExtensions(FilePath path, out string? extension) 25 | { 26 | extension = path.GetExtension(); 27 | 28 | while (path.HasExtension) 29 | { 30 | var current = path.GetExtension(); 31 | path = path.RemoveExtension(); 32 | 33 | if (current == ".received") 34 | { 35 | break; 36 | } 37 | } 38 | 39 | return path; 40 | } 41 | 42 | var path = StripExtensions(received, out var extension); 43 | var verifiedPath = path.AppendExtension(".verified"); 44 | if (extension != null) 45 | { 46 | verifiedPath = verifiedPath.AppendExtension(extension); 47 | } 48 | 49 | return verifiedPath; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Verify.Terminal/SnapshotDiff.cs: -------------------------------------------------------------------------------- 1 | namespace Verify.Terminal; 2 | 3 | public sealed class SnapshotDiff 4 | { 5 | public Snapshot Snapshot { get; } 6 | public List Old { get; } 7 | public List New { get; } 8 | 9 | public SnapshotDiff(Snapshot snapshot, List old, List @new) 10 | { 11 | Snapshot = snapshot.NotNull(); 12 | Old = old.NotNull(); 13 | New = @new.NotNull(); 14 | } 15 | 16 | public List<(int Start, int Stop)> GetRanges(int contextLines) 17 | { 18 | var index = 0; 19 | var start = default(int?); 20 | var ranges = new List<(int Start, int Stop)>(); 21 | 22 | // Only got a new file? 23 | if (Old.All(_ => _.Type == ChangeType.Imaginary)) 24 | { 25 | return [(0, New.Count)]; 26 | } 27 | 28 | while (index < New.Count) 29 | { 30 | var type = New[index].Type; 31 | if (type == ChangeType.Unchanged) 32 | { 33 | if (start != null) 34 | { 35 | // Found an unchanged line after something that 36 | // had been modified or deleted. 37 | var rangeStart = Math.Max(0, start.Value - contextLines); 38 | var rangeEnd = Math.Min(index + contextLines, New.Count - 1); 39 | ranges.Add((rangeStart, rangeEnd)); 40 | 41 | start = null; 42 | } 43 | } 44 | else 45 | { 46 | // Found a modified or deleted line. 47 | // If we're not currently processing a 48 | // modified or deleted line, set the start index. 49 | if (start == null) 50 | { 51 | start = index; 52 | } 53 | } 54 | 55 | index++; 56 | } 57 | 58 | if (ranges.Count == 0) 59 | { 60 | return new List<(int Start, int Stop)>(); 61 | } 62 | 63 | return MergeRanges(ranges); 64 | } 65 | 66 | private static List<(int Start, int Stop)> MergeRanges(List<(int Start, int Stop)> ranges) 67 | { 68 | var rangeCount = ranges.Count; 69 | var totalRanges = 1; 70 | 71 | for (var i = 0; i < rangeCount - 1; i++) 72 | { 73 | if (ranges[i].Stop >= ranges[i + 1].Start) 74 | { 75 | ranges[i + 1] = ( 76 | ranges[i].Start, 77 | Math.Max(ranges[i].Stop, ranges[i + 1].Stop)); 78 | 79 | ranges[i] = (-1, ranges[i].Stop); 80 | } 81 | else 82 | { 83 | totalRanges++; 84 | } 85 | } 86 | 87 | var index = 0; 88 | var ans = new int[totalRanges][]; 89 | for (var i = 0; i < rangeCount; i++) 90 | { 91 | if (ranges[i].Start != -1) 92 | { 93 | ans[index] = [0, 0]; 94 | 95 | ans[index][0] = ranges[i].Start; 96 | ans[index++][1] = ranges[i].Stop; 97 | } 98 | } 99 | 100 | var result = new List<(int, int)>(); 101 | for (var i = 0; i < ans.GetLength(0); i++) 102 | { 103 | result.Add((ans[i][0], ans[i][1])); 104 | } 105 | 106 | return result; 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/Verify.Terminal/SnapshotDiffAction.cs: -------------------------------------------------------------------------------- 1 | namespace Verify.Terminal; 2 | 3 | public enum SnapshotAction 4 | { 5 | Accept, 6 | Reject, 7 | Skip, 8 | } -------------------------------------------------------------------------------- /src/Verify.Terminal/SnapshotDiffer.cs: -------------------------------------------------------------------------------- 1 | namespace Verify.Terminal; 2 | 3 | public sealed class SnapshotDiffer 4 | { 5 | private readonly IFileSystem _fileSystem; 6 | private readonly IEnvironment _environment; 7 | 8 | public SnapshotDiffer(IFileSystem fileSystem, IEnvironment environment) 9 | { 10 | _fileSystem = fileSystem.NotNull(); 11 | _environment = environment.NotNull(); 12 | } 13 | 14 | public SnapshotDiff Diff(Snapshot snapshot) 15 | { 16 | var oldText = ReadText(snapshot.Verified) ?? string.Empty; 17 | var newText = ReadText(snapshot.Received) ?? string.Empty; 18 | 19 | var diff = SideBySideDiffBuilder.Instance.BuildDiffModel(oldText, newText, false); 20 | 21 | return new SnapshotDiff(snapshot, diff.OldText.Lines, diff.NewText.Lines); 22 | } 23 | 24 | private string? ReadText(FilePath path) 25 | { 26 | path = path.MakeAbsolute(_environment); 27 | 28 | if (!_fileSystem.File.Exists(path)) 29 | { 30 | return null; 31 | } 32 | 33 | using var stream = _fileSystem.File.OpenRead(path); 34 | using var reader = new StreamReader(stream); 35 | return reader.ReadToEnd(); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Verify.Terminal/SnapshotFinder.cs: -------------------------------------------------------------------------------- 1 | namespace Verify.Terminal; 2 | 3 | public sealed class SnapshotFinder 4 | { 5 | private readonly IFileSystem _fileSystem; 6 | private readonly IGlobber _globber; 7 | private readonly IEnvironment _environment; 8 | 9 | public SnapshotFinder( 10 | IFileSystem fileSystem, 11 | IGlobber globber, 12 | IEnvironment environment) 13 | { 14 | _fileSystem = fileSystem.NotNull(); 15 | _globber = globber.NotNull(); 16 | _environment = environment.NotNull(); 17 | } 18 | 19 | public ISet Find(DirectoryPath? root = null) 20 | { 21 | root ??= _environment.WorkingDirectory; 22 | root = root.MakeAbsolute(_environment); 23 | 24 | var result = new HashSet(); 25 | 26 | root = root.MakeAbsolute(_environment); 27 | var received = _globber.Match("**/*.received.*", new GlobberSettings 28 | { 29 | Root = root, 30 | }).Cast(); 31 | 32 | foreach (var receivedPath in received) 33 | { 34 | var (verifiedPath, isRerouted) = GetVerified(receivedPath); 35 | result.Add(new Snapshot(receivedPath, verifiedPath, isRerouted)); 36 | } 37 | 38 | return result; 39 | } 40 | 41 | private (FilePath VerifiedPath, bool IsRerouted) GetVerified(FilePath received) 42 | { 43 | var isRerouted = false; 44 | var path = StripExtensions(received, out var originalExtension); 45 | 46 | var extension = path.GetExtension(); 47 | if (extension != null) 48 | { 49 | if (extension.StartsWith(".DotNet") || 50 | extension.StartsWith(".Mono") || 51 | extension.StartsWith(".Net") || 52 | extension.StartsWith(".Core")) 53 | { 54 | var temp = path.RemoveExtension() 55 | .AppendExtension(".verified") 56 | .AppendExtensionIfNotNull(originalExtension); 57 | 58 | if (_fileSystem.File.Exists(temp)) 59 | { 60 | isRerouted = true; 61 | path = path.RemoveExtension(); 62 | } 63 | } 64 | } 65 | 66 | path = path 67 | .AppendExtension(".verified") 68 | .AppendExtensionIfNotNull(originalExtension); 69 | 70 | return (path, isRerouted); 71 | } 72 | 73 | private static FilePath StripExtensions(FilePath path, out string? originalExtension) 74 | { 75 | originalExtension = path.GetExtension(); 76 | 77 | while (path.HasExtension) 78 | { 79 | var current = path.GetExtension(); 80 | if (current == ".received") 81 | { 82 | path = path.RemoveExtension(); 83 | break; 84 | } 85 | 86 | path = path.RemoveExtension(); 87 | } 88 | 89 | return path; 90 | } 91 | } -------------------------------------------------------------------------------- /src/Verify.Terminal/SnapshotManager.cs: -------------------------------------------------------------------------------- 1 | namespace Verify.Terminal; 2 | 3 | public sealed class SnapshotManager 4 | { 5 | private readonly IFileSystem _fileSystem; 6 | 7 | public SnapshotManager(IFileSystem fileSystem) 8 | { 9 | _fileSystem = fileSystem; 10 | } 11 | 12 | public bool Process(Snapshot snapshot, SnapshotAction action) 13 | { 14 | return action switch 15 | { 16 | SnapshotAction.Accept => Accept(snapshot), 17 | SnapshotAction.Reject => Reject(snapshot), 18 | _ => throw new InvalidOperationException("Unknown snapshot action"), 19 | }; 20 | } 21 | 22 | public bool Accept(Snapshot snapshot) 23 | { 24 | try 25 | { 26 | // Delete the verified file 27 | if (_fileSystem.File.Exists(snapshot.Verified)) 28 | { 29 | _fileSystem.File.Delete(snapshot.Verified); 30 | if (_fileSystem.File.Exists(snapshot.Verified)) 31 | { 32 | // Could not delete the file 33 | return false; 34 | } 35 | } 36 | 37 | // Now move the file 38 | _fileSystem.File.Move(snapshot.Received, snapshot.Verified); 39 | } 40 | catch 41 | { 42 | return false; 43 | } 44 | 45 | return true; 46 | } 47 | 48 | public bool Reject(Snapshot snapshot) 49 | { 50 | try 51 | { 52 | // Delete the received file 53 | if (_fileSystem.File.Exists(snapshot.Received)) 54 | { 55 | _fileSystem.File.Delete(snapshot.Received); 56 | if (_fileSystem.File.Exists(snapshot.Received)) 57 | { 58 | // Could not delete the file 59 | return false; 60 | } 61 | } 62 | } 63 | catch 64 | { 65 | return false; 66 | } 67 | 68 | return true; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/Verify.Terminal/SnapshotRenderer.cs: -------------------------------------------------------------------------------- 1 | using Spectre.Console.Rendering; 2 | using Verify.Terminal.Rendering; 3 | 4 | namespace Verify.Terminal; 5 | 6 | internal sealed class SnapshotRendererContext 7 | { 8 | public SnapshotDiff Diff { get; } 9 | public ReportBuilder Builder { get; } 10 | public int LineNumberWidth { get; } 11 | 12 | public SnapshotRendererContext(SnapshotDiff diff, ReportBuilder builder) 13 | { 14 | Diff = diff.NotNull(); 15 | Builder = builder.NotNull(); 16 | LineNumberWidth = (int)(Math.Log10(Diff.New.Count) + 1); 17 | } 18 | } 19 | 20 | public class SnapshotRenderer 21 | { 22 | private readonly IAnsiConsole _console; 23 | 24 | public SnapshotRenderer(IAnsiConsole console) 25 | { 26 | _console = console; 27 | } 28 | 29 | public IRenderable Render(SnapshotDiff diff, int contextLines) 30 | { 31 | var characters = CharacterSet.Create(_console); 32 | var builder = new ReportBuilder(_console, characters); 33 | var ctx = new SnapshotRendererContext(diff, builder); 34 | 35 | var marginWidth = (ctx.LineNumberWidth * 2) + 8 + 1; 36 | var lineNumberWidth = (int)(Math.Log10(diff.New.Count) + 1); 37 | 38 | // Filename 39 | ctx.Builder.AppendRepeated(Character.HorizontalLine, _console.Profile.Width); 40 | ctx.Builder.CommitLine(); 41 | ctx.Builder.AppendInlineRenderable(new TextPath(diff.Snapshot.Received.GetFilename().FullPath)); 42 | 43 | if (diff.Snapshot.IsRerouted) 44 | { 45 | ctx.Builder.CommitLine(); 46 | ctx.Builder.AppendInlineRenderable(new TextPath(diff.Snapshot.Verified.GetFilename().FullPath)); 47 | 48 | ctx.Builder.AppendSpace(); 49 | ctx.Builder.Append("(rerouted)", Color.Yellow); 50 | } 51 | 52 | ctx.Builder.CommitLine(); 53 | 54 | // Legend 55 | ctx.Builder.AppendRepeated(Character.HorizontalLine, _console.Profile.Width); 56 | ctx.Builder.CommitLine(); 57 | ctx.Builder.Append("-old snapshot", Color.Red); 58 | ctx.Builder.CommitLine(); 59 | ctx.Builder.Append("+new snapshot", Color.Green); 60 | ctx.Builder.CommitLine(); 61 | 62 | // Top line 63 | ctx.Builder.AppendRepeated(Character.HorizontalLine, marginWidth); 64 | ctx.Builder.Append('┬'); 65 | ctx.Builder.Append(Character.HorizontalLine); 66 | ctx.Builder.Append('┬'); 67 | ctx.Builder.AppendRepeated(Character.HorizontalLine, _console.Profile.Width - (marginWidth + 3)); 68 | ctx.Builder.CommitLine(); 69 | 70 | var zip = diff.New.Zip(diff.Old).ToList(); 71 | 72 | // Iterate all ranges 73 | foreach (var (_, _, last, range) in diff.GetRanges(contextLines).Enumerate()) 74 | { 75 | for (var index = range.Start; index < range.Stop; index++) 76 | { 77 | var @new = zip[index].First; 78 | var old = zip[index].Second; 79 | 80 | if (@new.Type == ChangeType.Modified) 81 | { 82 | // Modified lines 83 | RenderLine(ctx, old, index, '-', Color.Red, true, false); 84 | RenderLine(ctx, @new, index, '+', Color.Green, false, true); 85 | } 86 | else if (@new.Type == ChangeType.Inserted) 87 | { 88 | // Inserted 89 | RenderLine(ctx, @new, index, '+', Color.Green, false, true); 90 | } 91 | else if (@new.Type == ChangeType.Imaginary) 92 | { 93 | // Modified lines 94 | RenderLine(ctx, old, index, '-', Color.Red, true, false); 95 | } 96 | else 97 | { 98 | // Unchanged 99 | RenderLine(ctx, @new, index, ' ', Color.Grey, true, true); 100 | } 101 | } 102 | 103 | // Bottom line 104 | ctx.Builder.AppendRepeated(Character.HorizontalLine, marginWidth); 105 | ctx.Builder.Append(last ? '┴' : '┼'); 106 | ctx.Builder.Append(Character.HorizontalLine); 107 | ctx.Builder.Append(last ? '┴' : '┼'); 108 | ctx.Builder.AppendRepeated(Character.HorizontalLine, _console.Profile.Width - (marginWidth + 3)); 109 | ctx.Builder.CommitLine(); 110 | } 111 | 112 | return ctx.Builder.Build(); 113 | } 114 | 115 | private void RenderLine( 116 | SnapshotRendererContext ctx, DiffPiece piece, 117 | int index, char op, Color color, 118 | bool showLeft, bool showRight) 119 | { 120 | var leftLineNumber = showLeft ? (index + 1).ToString() : string.Empty; 121 | var rightLineNumber = showRight ? (index + 1).ToString() : string.Empty; 122 | 123 | var maxWidth = _console.Profile.Width - (4 + ctx.LineNumberWidth + 4 + ctx.LineNumberWidth + 1 + 1 + 1 + 1); 124 | var pieces = (piece.Text.Length / maxWidth) + ((piece.Text.Length % maxWidth) == 0 ? 0 : 1); 125 | 126 | for (var i = 0; i < pieces; i++) 127 | { 128 | ctx.Builder.AppendSpaces(4); 129 | ctx.Builder.Append(leftLineNumber.PadLeft(ctx.LineNumberWidth), Color.Navy); 130 | ctx.Builder.AppendSpaces(4); 131 | ctx.Builder.Append(rightLineNumber.PadLeft(ctx.LineNumberWidth), Color.Navy); 132 | ctx.Builder.AppendSpace(); 133 | ctx.Builder.Append(Character.VerticalLine); 134 | 135 | if (i == 0) 136 | { 137 | ctx.Builder.Append(op, color); 138 | } 139 | else 140 | { 141 | ctx.Builder.Append(" "); 142 | } 143 | 144 | ctx.Builder.Append(Character.VerticalLine); 145 | 146 | var text = new string(piece.Text.Skip(i * maxWidth).Take(maxWidth).ToArray()); 147 | 148 | ctx.Builder.Append(text.Replace(" ", "·"), color); 149 | ctx.Builder.CommitLine(); 150 | } 151 | } 152 | } -------------------------------------------------------------------------------- /src/Verify.Terminal/Verify.Terminal.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net8.0;net7.0;net6.0 6 | true 7 | true 8 | Verify.Tool 9 | dotnet-verify 10 | 11 | 12 | 13 | A dotnet tool for managing Verify snapshots. 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /src/stylecop.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/DotNetAnalyzers/StyleCopAnalyzers/master/StyleCop.Analyzers/StyleCop.Analyzers/Settings/stylecop.schema.json", 3 | "settings": { 4 | "documentationRules": { 5 | "documentExposedElements": true, 6 | "documentInternalElements": false, 7 | "documentPrivateElements": false, 8 | "documentPrivateFields": false 9 | }, 10 | "layoutRules": { 11 | "newlineAtEndOfFile": "allow", 12 | "allowConsecutiveUsings": true 13 | }, 14 | "orderingRules": { 15 | "usingDirectivesPlacement": "outsideNamespace", 16 | "systemUsingDirectivesFirst": true, 17 | "elementOrder": [ 18 | "kind", 19 | "accessibility", 20 | "constant", 21 | "static", 22 | "readonly" 23 | ] 24 | } 25 | } 26 | } --------------------------------------------------------------------------------