├── .editorconfig ├── .gitattributes ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── TimeProviderExtensions.lutconfig ├── TimeProviderExtensions.sln ├── docs ├── FakeTimeProvider-advance-3-seconds.svg ├── ManualTimeProvider-advance-3-seconds.svg ├── System.Threading.PeriodicTimerWrapper.md ├── System.Threading.TimeProviderPeriodicTimerExtensions.md ├── TimeProviderExtensions.AutoAdvanceBehavior.md ├── TimeProviderExtensions.ManualTimeProvider.md ├── TimeProviderExtensions.ManualTimer.md ├── advance-1-second.svg ├── index.md └── jump-3-seconds.svg ├── key.snk ├── src └── TimeProviderExtensions │ ├── AutoAdvanceBehavior.cs │ ├── DefaultDocumentation.json │ ├── ManualTimeProvider.cs │ ├── ManualTimer.cs │ ├── ManualTimerScheduler.cs │ ├── System.Runtime.CompilerServices │ ├── CallerArgumentExpressionAttribute.cs │ └── IsExternalInit.cs │ ├── System.Threading │ ├── PeriodicTimerPort.cs │ ├── PeriodicTimerWrapper.cs │ └── TimeProviderPeriodicTimerExtensions.cs │ └── TimeProviderExtensions.csproj ├── stryker-config.json └── test └── TimeProviderExtensions.Tests ├── AutoAdvanceBehaviorTests.cs ├── FluentAssertions └── TaskAssertionsExtensions.cs ├── ManualTimeProviderCancelAfter.cs.cs ├── ManualTimeProviderDelayTests.cs ├── ManualTimeProviderPeriodicTimerTests.cs ├── ManualTimeProviderTests.cs ├── ManualTimeProviderTimestampTests.cs ├── ManualTimeProviderWaitAsyncTests.cs ├── ManualTimerTests.cs ├── Microsoft.Extensions.Time.Testing.Test ├── FakeTimeProviderTests.cs └── TimerTests.cs ├── TimeProviderExtensions.Tests.csproj └── Usings.cs /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | trim_trailing_whitespace = true 6 | insert_final_newline = true 7 | 8 | # C# files 9 | [*.cs] 10 | 11 | #### Core EditorConfig Options #### 12 | 13 | # Indentation and spacing 14 | indent_size = 4 15 | indent_style = space 16 | tab_width = 4 17 | 18 | # New line preferences 19 | end_of_line = crlf 20 | insert_final_newline = false 21 | 22 | #### .NET Coding Conventions #### 23 | 24 | # Organize usings 25 | dotnet_separate_import_directive_groups = false 26 | dotnet_sort_system_directives_first = true 27 | file_header_template = unset 28 | 29 | # this. and Me. preferences 30 | dotnet_style_qualification_for_event = false 31 | dotnet_style_qualification_for_field = false 32 | dotnet_style_qualification_for_method = false 33 | dotnet_style_qualification_for_property = false 34 | 35 | # Language keywords vs BCL types preferences 36 | dotnet_style_predefined_type_for_locals_parameters_members = true 37 | dotnet_style_predefined_type_for_member_access = true 38 | 39 | # Parentheses preferences 40 | dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity 41 | dotnet_style_parentheses_in_other_binary_operators = always_for_clarity 42 | dotnet_style_parentheses_in_other_operators = never_if_unnecessary 43 | dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity 44 | 45 | # Modifier preferences 46 | dotnet_style_require_accessibility_modifiers = for_non_interface_members 47 | 48 | # Expression-level preferences 49 | dotnet_style_coalesce_expression = true 50 | dotnet_style_collection_initializer = true 51 | dotnet_style_explicit_tuple_names = true 52 | dotnet_style_namespace_match_folder = true 53 | dotnet_style_null_propagation = true 54 | dotnet_style_object_initializer = true 55 | dotnet_style_operator_placement_when_wrapping = beginning_of_line 56 | dotnet_style_prefer_auto_properties = true 57 | dotnet_style_prefer_compound_assignment = true 58 | dotnet_style_prefer_conditional_expression_over_assignment = false 59 | dotnet_style_prefer_conditional_expression_over_return = true 60 | dotnet_style_prefer_foreach_explicit_cast_in_source = when_strongly_typed 61 | dotnet_style_prefer_inferred_anonymous_type_member_names = true 62 | dotnet_style_prefer_inferred_tuple_names = true 63 | dotnet_style_prefer_is_null_check_over_reference_equality_method = true 64 | dotnet_style_prefer_simplified_boolean_expressions = true 65 | dotnet_style_prefer_simplified_interpolation = true 66 | 67 | # Field preferences 68 | dotnet_style_readonly_field = true 69 | 70 | # Parameter preferences 71 | dotnet_code_quality_unused_parameters = all 72 | 73 | # Suppression preferences 74 | dotnet_remove_unnecessary_suppression_exclusions = none 75 | 76 | # New line preferences 77 | dotnet_style_allow_multiple_blank_lines_experimental = false:suggestion 78 | dotnet_style_allow_statement_immediately_after_block_experimental = true 79 | 80 | #### C# Coding Conventions #### 81 | 82 | # var preferences 83 | csharp_style_var_elsewhere = false 84 | csharp_style_var_for_built_in_types = false 85 | csharp_style_var_when_type_is_apparent = false 86 | 87 | # Expression-bodied members 88 | csharp_style_expression_bodied_accessors = true:suggestion 89 | csharp_style_expression_bodied_constructors = when_on_single_line:suggestion 90 | csharp_style_expression_bodied_indexers = true:suggestion 91 | csharp_style_expression_bodied_lambdas = true:suggestion 92 | csharp_style_expression_bodied_local_functions = true:suggestion 93 | csharp_style_expression_bodied_methods = when_on_single_line:suggestion 94 | csharp_style_expression_bodied_operators = when_on_single_line:suggestion 95 | csharp_style_expression_bodied_properties = true:suggestion 96 | 97 | # Pattern matching preferences 98 | csharp_style_pattern_matching_over_as_with_null_check = true 99 | csharp_style_pattern_matching_over_is_with_cast_check = true 100 | csharp_style_prefer_extended_property_pattern = true 101 | csharp_style_prefer_not_pattern = true 102 | csharp_style_prefer_pattern_matching = true 103 | csharp_style_prefer_switch_expression = true 104 | 105 | # Null-checking preferences 106 | csharp_style_conditional_delegate_call = false 107 | 108 | # Modifier preferences 109 | csharp_prefer_static_local_function = true 110 | csharp_preferred_modifier_order = public,private,protected,internal,file,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,required,volatile,async 111 | csharp_style_prefer_readonly_struct = true 112 | 113 | # Code-block preferences 114 | csharp_prefer_braces = when_multiline 115 | csharp_prefer_simple_using_statement = true 116 | csharp_style_namespace_declarations = file_scoped:warning 117 | csharp_style_prefer_method_group_conversion = true 118 | csharp_style_prefer_top_level_statements = true 119 | 120 | # Expression-level preferences 121 | csharp_prefer_simple_default_expression = true 122 | csharp_style_deconstructed_variable_declaration = true 123 | csharp_style_implicit_object_creation_when_type_is_apparent = true 124 | csharp_style_inlined_variable_declaration = true 125 | csharp_style_prefer_index_operator = true 126 | csharp_style_prefer_local_over_anonymous_function = true 127 | csharp_style_prefer_null_check_over_type_check = true 128 | csharp_style_prefer_range_operator = true 129 | csharp_style_prefer_tuple_swap = true 130 | csharp_style_prefer_utf8_string_literals = true 131 | csharp_style_throw_expression = true 132 | csharp_style_unused_value_assignment_preference = discard_variable 133 | csharp_style_unused_value_expression_statement_preference = discard_variable 134 | 135 | # 'using' directive preferences 136 | csharp_using_directive_placement = outside_namespace 137 | 138 | # New line preferences 139 | csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = false 140 | csharp_style_allow_blank_line_after_token_in_arrow_expression_clause_experimental = false:suggestion 141 | csharp_style_allow_blank_line_after_token_in_conditional_expression_experimental = false:suggestion 142 | csharp_style_allow_blank_lines_between_consecutive_braces_experimental = false 143 | csharp_style_allow_embedded_statements_on_same_line_experimental = false 144 | 145 | #### C# Formatting Rules #### 146 | 147 | # New line preferences 148 | csharp_new_line_before_catch = true 149 | csharp_new_line_before_else = true 150 | csharp_new_line_before_finally = true 151 | csharp_new_line_before_members_in_anonymous_types = true 152 | csharp_new_line_before_members_in_object_initializers = true 153 | csharp_new_line_before_open_brace = all 154 | csharp_new_line_between_query_expression_clauses = true 155 | 156 | # Indentation preferences 157 | csharp_indent_block_contents = true 158 | csharp_indent_braces = false 159 | csharp_indent_case_contents = false 160 | csharp_indent_case_contents_when_block = true 161 | csharp_indent_labels = no_change 162 | csharp_indent_switch_labels = true 163 | 164 | # Space preferences 165 | csharp_space_after_cast = false 166 | csharp_space_after_colon_in_inheritance_clause = true 167 | csharp_space_after_comma = true 168 | csharp_space_after_dot = false 169 | csharp_space_after_keywords_in_control_flow_statements = true 170 | csharp_space_after_semicolon_in_for_statement = true 171 | csharp_space_around_binary_operators = before_and_after 172 | csharp_space_around_declaration_statements = false 173 | csharp_space_before_colon_in_inheritance_clause = true 174 | csharp_space_before_comma = false 175 | csharp_space_before_dot = false 176 | csharp_space_before_open_square_brackets = false 177 | csharp_space_before_semicolon_in_for_statement = false 178 | csharp_space_between_empty_square_brackets = false 179 | csharp_space_between_method_call_empty_parameter_list_parentheses = false 180 | csharp_space_between_method_call_name_and_opening_parenthesis = false 181 | csharp_space_between_method_call_parameter_list_parentheses = false 182 | csharp_space_between_method_declaration_empty_parameter_list_parentheses = false 183 | csharp_space_between_method_declaration_name_and_open_parenthesis = false 184 | csharp_space_between_method_declaration_parameter_list_parentheses = false 185 | csharp_space_between_parentheses = false 186 | csharp_space_between_square_brackets = false 187 | 188 | # Wrapping preferences 189 | csharp_preserve_single_line_blocks = true 190 | csharp_preserve_single_line_statements = true 191 | 192 | #### Naming styles #### 193 | 194 | # Naming rules 195 | 196 | dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion 197 | dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface 198 | dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i 199 | 200 | dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion 201 | dotnet_naming_rule.types_should_be_pascal_case.symbols = types 202 | dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case 203 | 204 | dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion 205 | dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members 206 | dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case 207 | 208 | # Symbol specifications 209 | 210 | dotnet_naming_symbols.interface.applicable_kinds = interface 211 | dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected 212 | dotnet_naming_symbols.interface.required_modifiers = 213 | 214 | dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum 215 | dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected 216 | dotnet_naming_symbols.types.required_modifiers = 217 | 218 | dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method 219 | dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected 220 | dotnet_naming_symbols.non_field_members.required_modifiers = 221 | 222 | # Naming styles 223 | 224 | dotnet_naming_style.pascal_case.required_prefix = 225 | dotnet_naming_style.pascal_case.required_suffix = 226 | dotnet_naming_style.pascal_case.word_separator = 227 | dotnet_naming_style.pascal_case.capitalization = pascal_case 228 | 229 | dotnet_naming_style.begins_with_i.required_prefix = I 230 | dotnet_naming_style.begins_with_i.required_suffix = 231 | dotnet_naming_style.begins_with_i.word_separator = 232 | dotnet_naming_style.begins_with_i.capitalization = pascal_case 233 | 234 | #### Analyzer rules #### 235 | 236 | [*.{cs,vb}] 237 | dotnet_diagnostic.CA1014.severity = none 238 | dotnet_diagnostic.CA1510.severity = none # disabled because we are multi targeting frameworks that does not have ANE.ThrowIfNull 239 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | # yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json 2 | 3 | name: ci 4 | on: 5 | workflow_dispatch: # Allow running the workflow manually from the GitHub UI 6 | push: 7 | branches: 8 | - 'main' # Run the workflow when pushing to the main branch 9 | pull_request: 10 | branches: 11 | - '*' # Run the workflow for all pull requests 12 | release: 13 | types: 14 | - published # Run the workflow when a new GitHub release is published 15 | 16 | concurrency: 17 | group: ${{ github.workflow }}-${{ github.ref == 'refs/heads/main' && github.run_id || github.event.pull_request.number || github.ref }} 18 | cancel-in-progress: true 19 | 20 | env: 21 | DOTNET_SKIP_FIRST_TIME_EXPERIENCE: 1 22 | DOTNET_NOLOGO: true 23 | NuGetDirectory: ${{ github.workspace}}/nuget 24 | TestResultsDirectory: ${{ github.workspace}}/TestResults 25 | BRANCH_NAME: ${{ github.head_ref || github.ref_name }} 26 | 27 | jobs: 28 | create-nuget: 29 | runs-on: ubuntu-latest 30 | steps: 31 | - uses: actions/checkout@v3 32 | with: 33 | fetch-depth: 0 # Get all history to allow automatic versioning using MinVer 34 | 35 | # Install the .NET SDK indicated in the global.json file 36 | - name: Setup .NET 37 | uses: actions/setup-dotnet@v3 38 | with: 39 | dotnet-version: | 40 | 3.1.x 41 | 6.0.x 42 | 8.0.x 43 | 44 | # Create the NuGet package in the folder from the environment variable NuGetDirectory 45 | - run: dotnet pack --configuration Release --output ${{ env.NuGetDirectory }} 46 | 47 | # Publish the NuGet package as an artifact, so they can be used in the following jobs 48 | - uses: actions/upload-artifact@v3 49 | with: 50 | name: nuget 51 | if-no-files-found: error 52 | retention-days: 7 53 | path: ${{ env.NuGetDirectory }}/*.nupkg 54 | 55 | validate-nuget: 56 | runs-on: ubuntu-latest 57 | needs: [ create-nuget ] 58 | steps: 59 | - name: Setup .NET 60 | uses: actions/setup-dotnet@v3 61 | 62 | - uses: actions/download-artifact@v4.1.7 63 | with: 64 | name: nuget 65 | path: ${{ env.NuGetDirectory }} 66 | 67 | - name: Install nuget validator 68 | run: dotnet tool update Meziantou.Framework.NuGetPackageValidation.Tool --global 69 | 70 | # Validate metadata and content of the NuGet package 71 | # https://www.nuget.org/packages/Meziantou.Framework.NuGetPackageValidation.Tool#readme-body-tab 72 | # If some rules are not applicable, you can disable them 73 | # using the --excluded-rules or --excluded-rule-ids option 74 | - name: Validate package 75 | shell: pwsh 76 | run: meziantou.validate-nuget-package (Get-ChildItem "${{ env.NuGetDirectory }}/*.nupkg") --excluded-rules IconMustBeSet 77 | 78 | run-test: 79 | runs-on: ubuntu-latest 80 | timeout-minutes: 30 81 | strategy: 82 | matrix: 83 | framework: [ netcoreapp3.1, net6.0, net8.0 ] 84 | fail-fast: false 85 | env: 86 | TestResultsDirectory: ${{ github.workspace }}/TestResults 87 | permissions: 88 | checks: write 89 | steps: 90 | - uses: actions/checkout@v3 91 | 92 | - name: Setup .NET 93 | uses: actions/setup-dotnet@v3 94 | with: 95 | dotnet-version: | 96 | 3.1.x 97 | 6.0.x 98 | 8.0.x 99 | 100 | - name: Run tests 101 | run: dotnet test --configuration Release --framework ${{ matrix.framework }} --logger trx --results-directory "${{ env.TestResultsDirectory }}" --collect:"XPlat Code Coverage" --blame-hang --blame-hang-timeout 5min 102 | 103 | - uses: actions/upload-artifact@v3 104 | if: always() 105 | with: 106 | name: test-results-${{ matrix.framework }} 107 | if-no-files-found: error 108 | retention-days: 3 109 | path: ${{ env.TestResultsDirectory }}/**/* 110 | 111 | - name: Test Report 112 | uses: dorny/test-reporter@v1 113 | if: github.actor != 'dependabot[bot]' && (success() || failure()) && github.repository_owner == 'egil' 114 | with: 115 | name: test-results-${{ matrix.framework }} 116 | path: ${{ env.TestResultsDirectory }}/**/*.trx 117 | path-replace-backslashes: 'true' 118 | reporter: dotnet-trx 119 | 120 | run-stryker: 121 | runs-on: ubuntu-latest 122 | if: github.event_name != 'release' 123 | env: 124 | StrykerDirectory: ${{ github.workspace }}/Stryker 125 | permissions: 126 | statuses: write 127 | steps: 128 | - uses: actions/checkout@v3 129 | 130 | - name: Setup .NET 131 | uses: actions/setup-dotnet@v3 132 | with: 133 | dotnet-version: | 134 | 3.1.x 135 | 6.0.x 136 | 8.0.x 137 | 138 | - name: Install Stryker.NET 139 | run: dotnet tool install -g dotnet-stryker 140 | 141 | - name: Run Stryker.NET 142 | id: stryker 143 | run: | 144 | cd test/TimeProviderExtensions.Tests 145 | dotnet stryker --config-file "../../stryker-config.json" --dashboard-api-key "${{ secrets.STRYKER_DASHBOARD_API_KEY }}" --version ${{ env.BRANCH_NAME }} --output ${{ env.StrykerDirectory }} 146 | 147 | - run: | 148 | cat ${{ env.StrykerDirectory }}/reports/mutation-report.md >> $GITHUB_STEP_SUMMARY 149 | echo "" >> $GITHUB_STEP_SUMMARY 150 | echo "View the [full report](https://dashboard.stryker-mutator.io/reports/github.com/egil/TimeProviderExtensions/${{ env.BRANCH_NAME }})." >> $GITHUB_STEP_SUMMARY 151 | 152 | - name: Stryker Report 153 | if: github.actor != 'dependabot[bot]' && (success() || failure()) && github.repository_owner == 'egil' 154 | uses: Sibz/github-status-action@v1 155 | with: 156 | authToken: ${{secrets.GITHUB_TOKEN}} 157 | context: stryker-report" 158 | description: "See report" 159 | state: ${{ steps.stryker.conclusion }} 160 | sha: ${{ github.event.pull_request.head.sha || github.sha }} 161 | target_url: https://dashboard.stryker-mutator.io/reports/github.com/egil/TimeProviderExtensions/${{ env.BRANCH_NAME }} 162 | 163 | - uses: actions/upload-artifact@v3 164 | if: steps.stryker.conclusion == 'success' || steps.stryker.conclusion == 'failure' 165 | with: 166 | name: stryker-reports 167 | if-no-files-found: error 168 | retention-days: 3 169 | path: ${{ env.StrykerDirectory }}/**/* 170 | 171 | dependency-review: 172 | runs-on: ubuntu-latest 173 | permissions: 174 | contents: read 175 | if: github.event_name == 'pull_request' && github.repository_owner == 'egil' 176 | steps: 177 | - name: 'Checkout Repository' 178 | uses: actions/checkout@v3 179 | - name: 'Dependency Review' 180 | uses: actions/dependency-review-action@v3 181 | 182 | infer-sharp: 183 | runs-on: ubuntu-latest 184 | if: github.event_name != 'release' 185 | permissions: 186 | security-events: write 187 | steps: 188 | - uses: actions/checkout@v3 189 | with: 190 | fetch-depth: 0 191 | 192 | - name: Setup .NET 193 | uses: actions/setup-dotnet@v3 194 | with: 195 | dotnet-version: | 196 | 3.1.x 197 | 6.0.x 198 | 8.0.x 199 | 200 | - run: dotnet build --configuration Release 201 | 202 | - name: Run Infer# 203 | uses: microsoft/infersharpaction@v1.5 204 | id: runinfersharp 205 | with: 206 | binary-path: ./src/TimeProviderExtensions/bin/Release/net8.0 207 | github-sarif: true 208 | 209 | - name: Create step summary 210 | run: | 211 | echo # Infer# report >> $GITHUB_STEP_SUMMARY 212 | echo ``` >> $GITHUB_STEP_SUMMARY 213 | cat infer-out/report.txt >> $GITHUB_STEP_SUMMARY 214 | echo ``` >> $GITHUB_STEP_SUMMARY 215 | 216 | - name: Upload Infer# report as an artifact 217 | uses: actions/upload-artifact@v2 218 | with: 219 | name: infer-sharp-report 220 | path: infer-out/report.txt 221 | 222 | - name: Upload SARIF output to GitHub Security Center 223 | if: github.event_name == 'push' && github.ref == 'refs/heads/main' 224 | uses: github/codeql-action/upload-sarif@v2 225 | with: 226 | sarif_file: infer-out/report.sarif 227 | 228 | deploy: 229 | # Publish only when creating a GitHub Release 230 | # https://docs.github.com/en/repositories/releasing-projects-on-github/managing-releases-in-a-repository 231 | # You can update this logic if you want to manage releases differently 232 | if: github.event_name == 'release' 233 | runs-on: ubuntu-latest 234 | needs: [ validate-nuget, run-test ] 235 | steps: 236 | - uses: actions/download-artifact@v4.1.7 237 | with: 238 | name: nuget 239 | path: ${{ env.NuGetDirectory }} 240 | 241 | - name: Setup .NET Core 242 | uses: actions/setup-dotnet@v3 243 | 244 | - name: Publish NuGet package 245 | shell: pwsh 246 | run: | 247 | foreach($file in (Get-ChildItem "${{ env.NuGetDirectory }}" -Recurse -Include *.nupkg)) { 248 | dotnet nuget push $file --api-key "${{ secrets.NUGET_APIKEY }}" --source https://api.nuget.org/v3/index.json --skip-duplicate 249 | } 250 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Mono auto generated files 17 | mono_crash.* 18 | 19 | # Build results 20 | [Dd]ebug/ 21 | [Dd]ebugPublic/ 22 | [Rr]elease/ 23 | [Rr]eleases/ 24 | x64/ 25 | x86/ 26 | [Ww][Ii][Nn]32/ 27 | [Aa][Rr][Mm]/ 28 | [Aa][Rr][Mm]64/ 29 | bld/ 30 | [Bb]in/ 31 | [Oo]bj/ 32 | [Ll]og/ 33 | [Ll]ogs/ 34 | 35 | # Visual Studio 2015/2017 cache/options directory 36 | .vs/ 37 | # Uncomment if you have tasks that create the project's static files in wwwroot 38 | #wwwroot/ 39 | 40 | # Visual Studio 2017 auto generated files 41 | Generated\ Files/ 42 | 43 | # MSTest test Results 44 | [Tt]est[Rr]esult*/ 45 | [Bb]uild[Ll]og.* 46 | 47 | # NUnit 48 | *.VisualState.xml 49 | TestResult.xml 50 | nunit-*.xml 51 | 52 | # Build Results of an ATL Project 53 | [Dd]ebugPS/ 54 | [Rr]eleasePS/ 55 | dlldata.c 56 | 57 | # Benchmark Results 58 | BenchmarkDotNet.Artifacts/ 59 | 60 | # .NET 61 | project.lock.json 62 | project.fragment.lock.json 63 | artifacts/ 64 | 65 | # Tye 66 | .tye/ 67 | 68 | # ASP.NET Scaffolding 69 | ScaffoldingReadMe.txt 70 | 71 | # StyleCop 72 | StyleCopReport.xml 73 | 74 | # Files built by Visual Studio 75 | *_i.c 76 | *_p.c 77 | *_h.h 78 | *.ilk 79 | *.meta 80 | *.obj 81 | *.iobj 82 | *.pch 83 | *.pdb 84 | *.ipdb 85 | *.pgc 86 | *.pgd 87 | *.rsp 88 | *.sbr 89 | *.tlb 90 | *.tli 91 | *.tlh 92 | *.tmp 93 | *.tmp_proj 94 | *_wpftmp.csproj 95 | *.log 96 | *.tlog 97 | *.vspscc 98 | *.vssscc 99 | .builds 100 | *.pidb 101 | *.svclog 102 | *.scc 103 | 104 | # Chutzpah Test files 105 | _Chutzpah* 106 | 107 | # Visual C++ cache files 108 | ipch/ 109 | *.aps 110 | *.ncb 111 | *.opendb 112 | *.opensdf 113 | *.sdf 114 | *.cachefile 115 | *.VC.db 116 | *.VC.VC.opendb 117 | 118 | # Visual Studio profiler 119 | *.psess 120 | *.vsp 121 | *.vspx 122 | *.sap 123 | 124 | # Visual Studio Trace Files 125 | *.e2e 126 | 127 | # TFS 2012 Local Workspace 128 | $tf/ 129 | 130 | # Guidance Automation Toolkit 131 | *.gpState 132 | 133 | # ReSharper is a .NET coding add-in 134 | _ReSharper*/ 135 | *.[Rr]e[Ss]harper 136 | *.DotSettings.user 137 | 138 | # TeamCity is a build add-in 139 | _TeamCity* 140 | 141 | # DotCover is a Code Coverage Tool 142 | *.dotCover 143 | 144 | # AxoCover is a Code Coverage Tool 145 | .axoCover/* 146 | !.axoCover/settings.json 147 | 148 | # Coverlet is a free, cross platform Code Coverage Tool 149 | coverage*.json 150 | coverage*.xml 151 | coverage*.info 152 | 153 | # Visual Studio code coverage results 154 | *.coverage 155 | *.coveragexml 156 | 157 | # NCrunch 158 | _NCrunch_* 159 | .*crunch*.local.xml 160 | nCrunchTemp_* 161 | 162 | # MightyMoose 163 | *.mm.* 164 | AutoTest.Net/ 165 | 166 | # Web workbench (sass) 167 | .sass-cache/ 168 | 169 | # Installshield output folder 170 | [Ee]xpress/ 171 | 172 | # DocProject is a documentation generator add-in 173 | DocProject/buildhelp/ 174 | DocProject/Help/*.HxT 175 | DocProject/Help/*.HxC 176 | DocProject/Help/*.hhc 177 | DocProject/Help/*.hhk 178 | DocProject/Help/*.hhp 179 | DocProject/Help/Html2 180 | DocProject/Help/html 181 | 182 | # Click-Once directory 183 | publish/ 184 | 185 | # Publish Web Output 186 | *.[Pp]ublish.xml 187 | *.azurePubxml 188 | # Note: Comment the next line if you want to checkin your web deploy settings, 189 | # but database connection strings (with potential passwords) will be unencrypted 190 | *.pubxml 191 | *.publishproj 192 | 193 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 194 | # checkin your Azure Web App publish settings, but sensitive information contained 195 | # in these scripts will be unencrypted 196 | PublishScripts/ 197 | 198 | # NuGet Packages 199 | *.nupkg 200 | # NuGet Symbol Packages 201 | *.snupkg 202 | # The packages folder can be ignored because of Package Restore 203 | **/[Pp]ackages/* 204 | # except build/, which is used as an MSBuild target. 205 | !**/[Pp]ackages/build/ 206 | # Uncomment if necessary however generally it will be regenerated when needed 207 | #!**/[Pp]ackages/repositories.config 208 | # NuGet v3's project.json files produces more ignorable files 209 | *.nuget.props 210 | *.nuget.targets 211 | 212 | # Microsoft Azure Build Output 213 | csx/ 214 | *.build.csdef 215 | 216 | # Microsoft Azure Emulator 217 | ecf/ 218 | rcf/ 219 | 220 | # Windows Store app package directories and files 221 | AppPackages/ 222 | BundleArtifacts/ 223 | Package.StoreAssociation.xml 224 | _pkginfo.txt 225 | *.appx 226 | *.appxbundle 227 | *.appxupload 228 | 229 | # Visual Studio cache files 230 | # files ending in .cache can be ignored 231 | *.[Cc]ache 232 | # but keep track of directories ending in .cache 233 | !?*.[Cc]ache/ 234 | 235 | # Others 236 | ClientBin/ 237 | ~$* 238 | *~ 239 | *.dbmdl 240 | *.dbproj.schemaview 241 | *.jfm 242 | *.pfx 243 | *.publishsettings 244 | orleans.codegen.cs 245 | 246 | # Including strong name files can present a security risk 247 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 248 | #*.snk 249 | 250 | # Since there are multiple workflows, uncomment next line to ignore bower_components 251 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 252 | #bower_components/ 253 | 254 | # RIA/Silverlight projects 255 | Generated_Code/ 256 | 257 | # Backup & report files from converting an old project file 258 | # to a newer Visual Studio version. Backup files are not needed, 259 | # because we have git ;-) 260 | _UpgradeReport_Files/ 261 | Backup*/ 262 | UpgradeLog*.XML 263 | UpgradeLog*.htm 264 | ServiceFabricBackup/ 265 | *.rptproj.bak 266 | 267 | # SQL Server files 268 | *.mdf 269 | *.ldf 270 | *.ndf 271 | 272 | # Business Intelligence projects 273 | *.rdl.data 274 | *.bim.layout 275 | *.bim_*.settings 276 | *.rptproj.rsuser 277 | *- [Bb]ackup.rdl 278 | *- [Bb]ackup ([0-9]).rdl 279 | *- [Bb]ackup ([0-9][0-9]).rdl 280 | 281 | # Microsoft Fakes 282 | FakesAssemblies/ 283 | 284 | # GhostDoc plugin setting file 285 | *.GhostDoc.xml 286 | 287 | # Node.js Tools for Visual Studio 288 | .ntvs_analysis.dat 289 | node_modules/ 290 | 291 | # Visual Studio 6 build log 292 | *.plg 293 | 294 | # Visual Studio 6 workspace options file 295 | *.opt 296 | 297 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 298 | *.vbw 299 | 300 | # Visual Studio 6 auto-generated project file (contains which files were open etc.) 301 | *.vbp 302 | 303 | # Visual Studio 6 workspace and project file (working project files containing files to include in project) 304 | *.dsw 305 | *.dsp 306 | 307 | # Visual Studio 6 technical files 308 | *.ncb 309 | *.aps 310 | 311 | # Visual Studio LightSwitch build output 312 | **/*.HTMLClient/GeneratedArtifacts 313 | **/*.DesktopClient/GeneratedArtifacts 314 | **/*.DesktopClient/ModelManifest.xml 315 | **/*.Server/GeneratedArtifacts 316 | **/*.Server/ModelManifest.xml 317 | _Pvt_Extensions 318 | 319 | # Paket dependency manager 320 | .paket/paket.exe 321 | paket-files/ 322 | 323 | # FAKE - F# Make 324 | .fake/ 325 | 326 | # CodeRush personal settings 327 | .cr/personal 328 | 329 | # Python Tools for Visual Studio (PTVS) 330 | __pycache__/ 331 | *.pyc 332 | 333 | # Cake - Uncomment if you are using it 334 | # tools/** 335 | # !tools/packages.config 336 | 337 | # Tabs Studio 338 | *.tss 339 | 340 | # Telerik's JustMock configuration file 341 | *.jmconfig 342 | 343 | # BizTalk build output 344 | *.btp.cs 345 | *.btm.cs 346 | *.odx.cs 347 | *.xsd.cs 348 | 349 | # OpenCover UI analysis results 350 | OpenCover/ 351 | 352 | # Azure Stream Analytics local run output 353 | ASALocalRun/ 354 | 355 | # MSBuild Binary and Structured Log 356 | *.binlog 357 | 358 | # NVidia Nsight GPU debugger configuration file 359 | *.nvuser 360 | 361 | # MFractors (Xamarin productivity tool) working folder 362 | .mfractor/ 363 | 364 | # Local History for Visual Studio 365 | .localhistory/ 366 | 367 | # Visual Studio History (VSHistory) files 368 | .vshistory/ 369 | 370 | # BeatPulse healthcheck temp database 371 | healthchecksdb 372 | 373 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 374 | MigrationBackup/ 375 | 376 | # Ionide (cross platform F# VS Code tools) working folder 377 | .ionide/ 378 | 379 | # Fody - auto-generated XML schema 380 | FodyWeavers.xsd 381 | 382 | # VS Code files for those working on multiple tools 383 | .vscode/* 384 | !.vscode/settings.json 385 | !.vscode/tasks.json 386 | !.vscode/launch.json 387 | !.vscode/extensions.json 388 | *.code-workspace 389 | 390 | # Local History for Visual Studio Code 391 | .history/ 392 | 393 | # Windows Installer files from build outputs 394 | *.cab 395 | *.msi 396 | *.msix 397 | *.msm 398 | *.msp 399 | 400 | # JetBrains Rider 401 | *.sln.iml 402 | 403 | ## 404 | ## Visual studio for Mac 405 | ## 406 | 407 | 408 | # globs 409 | Makefile.in 410 | *.userprefs 411 | *.usertasks 412 | config.make 413 | config.status 414 | aclocal.m4 415 | install-sh 416 | autom4te.cache/ 417 | *.tar.gz 418 | tarballs/ 419 | test-results/ 420 | 421 | # Mac bundle stuff 422 | *.dmg 423 | *.app 424 | 425 | # content below from: https://github.com/github/gitignore/blob/master/Global/macOS.gitignore 426 | # General 427 | .DS_Store 428 | .AppleDouble 429 | .LSOverride 430 | 431 | # Icon must end with two \r 432 | Icon 433 | 434 | 435 | # Thumbnails 436 | ._* 437 | 438 | # Files that might appear in the root of a volume 439 | .DocumentRevisions-V100 440 | .fseventsd 441 | .Spotlight-V100 442 | .TemporaryItems 443 | .Trashes 444 | .VolumeIcon.icns 445 | .com.apple.timemachine.donotpresent 446 | 447 | # Directories potentially created on remote AFP share 448 | .AppleDB 449 | .AppleDesktop 450 | Network Trash Folder 451 | Temporary Items 452 | .apdisk 453 | 454 | # content below from: https://github.com/github/gitignore/blob/master/Global/Windows.gitignore 455 | # Windows thumbnail cache files 456 | Thumbs.db 457 | ehthumbs.db 458 | ehthumbs_vista.db 459 | 460 | # Dump file 461 | *.stackdump 462 | 463 | # Folder config file 464 | [Dd]esktop.ini 465 | 466 | # Recycle Bin used on file shares 467 | $RECYCLE.BIN/ 468 | 469 | # Windows Installer files 470 | *.cab 471 | *.msi 472 | *.msix 473 | *.msm 474 | *.msp 475 | 476 | # Windows shortcuts 477 | *.lnk 478 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to TimeProviderExtensions will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [1.0.0] 9 | 10 | - Upgrade dependencies to none-preview versions. 11 | 12 | ## [1.0.0-rc.3] 13 | 14 | - Generate strong-named assemblies. 15 | 16 | ## [1.0.0-rc.2] 17 | 18 | - Added `ActiveTimers` property to `ManualTimeProvider`. The property will display the number of currently active timers that have a callback scheduled to be called in the future. 19 | 20 | - Allow `ManualTimeProvider.Start` to be set using property initializers. 21 | 22 | - Made the timer type created by `ManualTimeProvider`, the `ManualTimer` type, public, and introduced a protected method `CreateManualTimer` on `ManualTimeProvider`. This enables advanced scenarios where a custom `ManualTimer` is needed. 23 | 24 | A custom implementation of `ManualTimer` can override the `Change` method and add custom behavior to it. 25 | 26 | Overriding `CreateManualTimer` makes it possible to intercept a `TimerCallback` and perform actions before and after the timer callback has been invoked. 27 | 28 | - Replace the `AutoAdvanceAmount` property with the `AutoAdvanceBehavior` property on `ManualTimeProvider`, and introduce the `AutoAdvanceBehavior` type. To automatically advance the time when `GetUtcNow()` or `GetLocalNow()` is called, set `AutoAdvanceBehavior.UtcNowAdvanceAmount` to a `TimeSpan` larger than zero. 29 | 30 | - Enable auto advance feature for `GetTimestamp()` and `GetElapsedTime(long)`. To automatically advance the time when `GetTimestamp()` or `GetElapsedTime(long)` is called, set `AutoAdvanceBehavior.TimestampAdvanceAmount` to a `TimeSpan` larger than zero. 31 | 32 | - `ManualTimer` now exposes its current configuration. `DueTime`, `Period`, `IsActive`, `CallbackTime`, and `CallbackInvokeCount` are now publicly visible. 33 | 34 | - Enable auto-advance feature for timers. This enables automatically calling timers callback a specified number of times, by setting the `AutoAdvanceBehavior.TimerAutoTriggerCount` property to a number larger than zero. 35 | 36 | ## [1.0.0-rc.1] 37 | 38 | - Updated Microsoft.Bcl.TimeProvider package dependency to rc.1 version. 39 | 40 | ## [1.0.0-preview.7] 41 | 42 | - Added support for netstandard2.0, as this is supported by the back-port package https://www.nuget.org/packages/Microsoft.Bcl.TimeProvider. 43 | 44 | ## [1.0.0-preview.6] 45 | 46 | - Added `Jump(TimeSpan)` and `Jump(DateTimeOffset)` methods that will jump time to the specified place. Any timer callbacks between the start and end of the jump will be invoked the expected number of times, but the date/time returned from `GetUtcNow()` and `GetTimestamp()` will always be the jump time. This differs from how `Advance` and `SetUtcNow` works. See the readme for a detailed description. 47 | 48 | ## [1.0.0-preview.5] 49 | 50 | Aligned the public API surface of `ManualTimeProvider` with `Microsoft.Extensions.Time.Testing.FakeTimeProvider`. This means: 51 | 52 | - The `StartTime` property is now called `Start`. 53 | - The `ForwardTime` method has been removed (use `Advance` instead). 54 | - The `AutoAdvanceAmount` property has been introduced, which will advance time with the specified amount every time `GetUtcNow()` is called. It defaults to `TimeSpan.Zero`, which disables auto-advancing. 55 | 56 | ## [1.0.0-preview.4] 57 | 58 | - Added 'StartTime' to `ManualTestProvider`, which represents the initial date/time when the `ManualtTimeProvider` was initialized. 59 | 60 | ## [1.0.0-preview.3] 61 | 62 | - Changed `ManualTestProvider` to set the local time zone to UTC by default, providing a method for overriding during testing. 63 | 64 | - Changed the `ManualTestProvider.ToString()` method to return current date time. 65 | 66 | - Fixed `ITimer` returned by `ManualTestProvider` such that timers created with a due time equal to zero will fire the timer callback immediately. 67 | 68 | ## [1.0.0-preview.1] 69 | 70 | This release adds a dependency on [Microsoft.Bcl.TimeProvider](https://www.nuget.org/packages/Microsoft.Bcl.TimeProvider) and utilizes the types built-in to that to do much of the work. 71 | 72 | When using the `ManualTimeProvider` during testing, be aware of these outstanding issues: https://github.com/dotnet/runtime/issues/85326 73 | 74 | - Removed `CancelAfter` extension methods. Instead, create a CancellationTokenSource via the method `TimeProvider.CreateCancellationTokenSource(TimeSpan delay)` or in .NET 8, using `new CancellationTokenSource(TimeSpan delay, TimeProvider timeProvider). 75 | 76 | **NOTE:** If running on .NET versions earlier than .NET 8.0, there is a constraint when invoking `CancellationTokenSource.CancelAfter(TimeSpan)` on the resultant object. This action will not terminate the initial timer indicated by `delay`. However, this restriction does not apply to .NET 8.0 and later versions. 77 | 78 | ## [0.8.0] 79 | 80 | - Added `TimeProvider.GetElapsedTime(long startingTimestamp)` 81 | - Added `TimeProvider.CreateCancellationTokenSource(TimeSpan delay)` 82 | 83 | ## [0.7.0] 84 | 85 | - Add support for libraries that target netstandard 2.0. 86 | 87 | ## [0.6.0] 88 | 89 | - Changed `TestTimeProvider` to `ManualTimeProvider`. 90 | - `ManualTimeProvider` no longer implements on `IDisposable`. 91 | - Moving time forward using `ManualTimeProvider` will now move time forward in steps, stopping at each scheduled timer/callback time, setting the internal "UtcNow" clock returned from `GetUtcNow()` to invoke the callback, and then progress to the next scheduled timer, until the target "UtcNow" is reached. 92 | 93 | ## [0.5.0] 94 | 95 | - Implemented a shim for the TimeProvider API coming in .NET 8. 96 | - Added support for controlling timestamps during testing. 97 | - Marked the `UtcNow` as obsolete. 98 | 99 | ## [0.4.0] 100 | 101 | - Added support for timers. 102 | 103 | ## [0.3.0] - 2023-03-03 104 | 105 | ### Added 106 | 107 | - Adds support for canceling a `CancellationTokenSource` after a specific timespan via the `ITimeScheduler.CancelAfter(CancellationTokenSource, TimeSpan)` method. 108 | - Adds a singleton instance property to `DefaultScheduler` that can be used instead of creating a new instance for every use. 109 | 110 | ### Changed 111 | 112 | - All methods in `DefaultScheduler` marked with the `[MethodImpl(MethodImplOptions.AggressiveInlining)]` attribute. 113 | - `TestScheduler.ForwardTime(TimeSpan time)` throws `ArgumentException` if the `time` argument is not positive. 114 | 115 | ## [0.2.0] - 2023-02-21 116 | 117 | Adds support for the `Task.WaitAsync` family of methods. 118 | 119 | ## [0.1.3-preview] - 2023-01-30 120 | 121 | Initial release with support for `Task.Delay` and `PeriodicTimer`. 122 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Egil Hansen 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 | [![GitHub tag (latest SemVer pre-release)](https://img.shields.io/github/v/tag/egil/TimeProviderExtensions?include_prereleases&logo=github&style=flat-square)](https://github.com/egil/TimeProviderExtensions/releases) 2 | [![Nuget](https://img.shields.io/nuget/dt/TimeProviderExtensions?logo=nuget&style=flat-square)](https://www.nuget.org/packages/TimeProviderExtensions/) 3 | [![Issues Open](https://img.shields.io/github/issues/egil/TimeProviderExtensions.svg?style=flat-square&logo=github)](https://github.com/egil/TimeProviderExtensions/issues) 4 | [![Mutation testing badge](https://img.shields.io/endpoint?style=flat&url=https%3A%2F%2Fbadge-api.stryker-mutator.io%2Fgithub.com%2Fegil%2FTimeProviderExtensions%2Fmain)](https://dashboard.stryker-mutator.io/reports/github.com/egil/TimeProviderExtensions/main) 5 | 6 | # TimeProvider Extensions 7 | 8 | Testing extensions for the [`System.TimeProvider`](https://learn.microsoft.com/en-us/dotnet/api/system.timeprovider) API. It includes: 9 | 10 | - An advanced test/fake version of the `TimeProvider` type, named `ManualTimeProvider`, that allows you to control the progress of time during testing deterministically (see the difference to Microsoft's `FakeTimeProvider` below). 11 | - A backported version of `PeriodicTimer` that supports `TimeProvider` in .NET 6. 12 | 13 | ## Quick start 14 | 15 | This describes how to get started: 16 | 17 | 1. Get the latest release from https://www.nuget.org/packages/TimeProviderExtensions. 18 | 19 | 2. Take a dependency on `TimeProvider` in your production code. Inject the production version of `TimeProvider` available via the [`TimeProvider.System`](https://learn.microsoft.com/en-us/dotnet/api/system.timeprovider.system?#system-timeprovider-system) property during production. 20 | 21 | 3. During testing, inject the `ManualTimeProvider` from this library. This allows you to write tests that run fast and predictably. 22 | - Advance time by calling `Advance(TimeSpan)` or `SetUtcNow(DateTimeOffset)` or 23 | - Jump ahead in time using `Jump(TimeSpan)` or `Jump(DateTimeOffset)`. 24 | 25 | 4. See the **[`ManualTimeProvider API`](https://github.com/egil/TimeProviderExtensions/blob/main/docs/TimeProviderExtensions.ManualTimeProvider.md) page** for the full API documentation for `ManualTimeProvider`. 26 | 27 | 5. Read the rest of this README for further details and examples. 28 | 29 | ## API Overview 30 | 31 | These pages have all the details of the API included in this package: 32 | 33 | - [`ManualTimeProvider`](https://github.com/egil/TimeProviderExtensions/blob/main/docs/TimeProviderExtensions.ManualTimeProvider.md) 34 | - [`AutoAdvanceBehavior`](https://github.com/egil/TimeProviderExtensions/blob/main/docs/TimeProviderExtensions.AutoAdvanceBehavior.md) 35 | - [`ManualTimer`](https://github.com/egil/TimeProviderExtensions/blob/main/docs/TimeProviderExtensions.ManualTimer.md) 36 | 37 | **.NET 7 and earlier:** 38 | 39 | - [`PeriodicTimerWrapper`](https://github.com/egil/TimeProviderExtensions/blob/main/docs/System.Threading.PeriodicTimerWrapper.md) 40 | - [`TimeProviderPeriodicTimerExtensions`](https://github.com/egil/TimeProviderExtensions/blob/main/docs/System.Threading.TimeProviderPeriodicTimerExtensions.md) 41 | 42 | ## Known limitations and issues: 43 | 44 | - If running on .NET versions earlier than .NET 8.0, there is a constraint when invoking `CancellationTokenSource.CancelAfter(TimeSpan)` on the `CancellationTokenSource` object returned by `CreateCancellationTokenSource(TimeSpan delay)`. This action will not terminate the initial timer indicated by the `delay` argument initially passed the `CreateCancellationTokenSource` method. However, this restriction does not apply to .NET 8.0 and later versions. 45 | - To enable controlling `PeriodicTimer` via `TimeProvider` in versions of .NET earlier than .NET 8.0, the `TimeProvider.CreatePeriodicTimer` returns a `PeriodicTimerWrapper` object instead of a `PeriodicTimer` object. The `PeriodicTimerWrapper` type is just a lightweight wrapper around the original `System.Threading.PeriodicTimer` and will behave identically to it. 46 | - If `ManualTimeProvider` is created via [AutoFixture](https://github.com/AutoFixture/AutoFixture), be aware that will set writable properties with random values. This behavior can be overridden by providing a customization to AutoFixture, e.g.: 47 | ```c# 48 | fixture.Customize(x => x.OmitAutoProperties())); 49 | ``` 50 | or by using the `[NoAutoProperties]` attribute, if using `AutoFixture.Xunit2`. 51 | 52 | ## Installation and Usage 53 | 54 | Get the latest release from https://www.nuget.org/packages/TimeProviderExtensions 55 | 56 | ### Set up in production 57 | 58 | To use in production, pass in `TimeProvider.System` to the types that depend on `TimeProvider`. 59 | This can be done directly or via an IoC Container, e.g., .NETs built-in `IServiceCollection` like so: 60 | 61 | ```c# 62 | services.AddSingleton(TimeProvider.System); 63 | ``` 64 | 65 | If you do not want to register the `TimeProvider` with your IoC container, you can instead create 66 | an additional constructor in the types that use it, which allows you to pass in a `TimeProvider`, 67 | and in the existing constructor(s) you have, just new up `TimeProvider.System` directly. For example: 68 | 69 | ```c# 70 | public class MyService 71 | { 72 | private readonly TimeProvider timeProvider; 73 | 74 | public MyService() : this(TimeProvider.System) 75 | { 76 | } 77 | 78 | public MyService(TimeProvider timeProvider) 79 | { 80 | this.timeProvider = timeProvider; 81 | } 82 | } 83 | ``` 84 | 85 | This allows you to explicitly pass in a `ManualTimeProvider` during testing. 86 | 87 | ### Example - control time during tests 88 | 89 | If a system under test (SUT) uses things like `Task.Delay`, `DateTimeOffset.UtcNow`, `Task.WaitAsync`, or `PeriodicTimer`, 90 | it becomes hard to create tests that run fast and predictably. 91 | 92 | The idea is to replace the use of e.g. `Task.Delay` with an abstraction, the `TimeProvider`, that in production 93 | is represented by the `TimeProvider.System`, which just uses the real `Task.Delay`. During testing it is now possible to 94 | pass in `ManualTimeProvider`, which allows the test to control the progress of time, making it possible to skip ahead, 95 | e.g. 10 minutes, and also pause time, leading to fast and predictable tests. 96 | 97 | As an example, let us test the "Stuff Service" below that performs specific tasks every 10 seconds with an additional 98 | 1-second delay. We have two versions, one that uses the standard types in .NET, and one that uses the `TimeProvider`. 99 | 100 | ```c# 101 | // Version of stuff service that uses the built-in DateTimeOffset, PeriodicTimer, and Task.Delay 102 | public class StuffService 103 | { 104 | private static readonly TimeSpan doStuffDelay = TimeSpan.FromSeconds(10); 105 | private readonly List container; 106 | 107 | public StuffService(List container) 108 | { 109 | this.container = container; 110 | } 111 | 112 | public async Task DoStuff(CancellationToken cancelllationToken) 113 | { 114 | using var periodicTimer = new PeriodicTimer(doStuffDelay); 115 | 116 | while (await periodicTimer.WaitForNextTickAsync(cancellationToken)) 117 | { 118 | await Task.Delay(TimeSpan.FromSeconds(1)); 119 | container.Add(DateTimeOffset.UtcNow); 120 | } 121 | } 122 | } 123 | 124 | // Version of stuff service that uses the built-in TimeProvider 125 | public class StuffServiceUsingTimeProvider 126 | { 127 | private static readonly TimeSpan doStuffDelay = TimeSpan.FromSeconds(10); 128 | private readonly TimeProvider timeProvider; 129 | private readonly List container; 130 | 131 | public StuffServiceUsingTimeProvider(TimeProvider timeProvider, List container) 132 | { 133 | this.timeProvider = timeProvider; 134 | this.container = container; 135 | } 136 | 137 | public async Task DoStuff(CancellationToken cancelllationToken) 138 | { 139 | using var periodicTimer = timeProvider.CreatePeriodicTimer(doStuffDelay); 140 | 141 | while (await periodicTimer.WaitForNextTickAsync(cancellationToken)) 142 | { 143 | await timeProvider.Delay(TimeSpan.FromSeconds(1)); 144 | container.Add(timeProvider.GetUtcNow()); 145 | } 146 | } 147 | } 148 | ``` 149 | 150 | The test, using xUnit and FluentAssertions, could look like this: 151 | 152 | ```c# 153 | [Fact] 154 | public void DoStuff_does_stuff_every_11_seconds() 155 | { 156 | // Arrange 157 | var timeProvider = new ManualTimeProvider(); 158 | var container = new List(); 159 | var sut = new StuffServiceUsingTimeProvider(timeProvider, container); 160 | 161 | // Act 162 | _ = sut.DoStuff(CancellationToken.None); 163 | timeProvider.Advance(TimeSpan.FromSeconds(11)); 164 | 165 | // Assert 166 | container 167 | .Should() 168 | .ContainSingle() 169 | .Which 170 | .Should() 171 | .Be(timeProvider.GetUtcNow()); 172 | } 173 | ``` 174 | 175 | This test will run in nanoseconds and is deterministic. 176 | 177 | Compare that to the similar test below for `StuffService` that needs to wait for 11 seconds before it can safely assert that the expectation has been met. 178 | 179 | ```c# 180 | [Fact] 181 | public async Task DoStuff_does_stuff_every_11_seconds() 182 | { 183 | // Arrange 184 | var container = new List(); 185 | var sut = new StuffService(container); 186 | 187 | // Act 188 | _ = sut.DoStuff(CancellationToken.None); 189 | await Task.Delay(TimeSpan.FromSeconds(11)); 190 | 191 | // Assert 192 | container 193 | .Should() 194 | .ContainSingle() 195 | .Which 196 | .Should() 197 | .BeCloseTo(DateTimeOffset.UtcNow, precision: TimeSpan.FromMilliseconds(50)); 198 | } 199 | ``` 200 | 201 | ## Difference between `ManualTimeProvider` and `FakeTimeProvider` 202 | 203 | The .NET team has published a similar test-specific time provider, the [`Microsoft.Extensions.Time.Testing.FakeTimeProvider`](https://www.nuget.org/packages/Microsoft.Extensions.TimeProvider.Testing). 204 | 205 | The public API of both `FakeTimeProvider` and `ManualTimeProvider` are compatible, but there are some differences in when time is set before timer callbacks. Let's illustrate this with an example: 206 | 207 | For example, if we create an `ITimer` with a *due time* and *period* set to **1 second**, the `DateTimeOffset` returned from `GetUtcNow()` during the timer callback may be different depending on the amount passed to `Advance()` (or `SetUtcNow()`). 208 | 209 | If we call `Advance(TimeSpan.FromSeconds(1))` three times, effectively moving time forward by three seconds, the timer callback will be invoked once at times `00:01`, `00:02`, and `00:03`, as illustrated in the drawing below. Both `FakeTimeProvider` and `ManualTimeProvider` behave like this: 210 | 211 | ![Advancing time by three seconds in one-second increments.](https://raw.githubusercontent.com/egil/TimeProviderExtensions/main/docs/advance-1-second.svg) 212 | 213 | If we instead call `Advance(TimeSpan.FromSeconds(3))` once, the two implementations behave differently. `ManualTimeProvider` will invoke the timer callback at the same time (`00:01`, `00:02`, and `00:03`) as if we had called `Advance(TimeSpan.FromSeconds(1))` three times, as illustrated in the drawing below: 214 | 215 | ![Advancing time by three seconds in one step using ManualTimeProvider.](https://raw.githubusercontent.com/egil/TimeProviderExtensions/main/docs/ManualTimeProvider-advance-3-seconds.svg) 216 | 217 | However, `FakeTimeProvider` will invoke the timer callback at time `00:03` three times, as illustrated in the drawings below: 218 | 219 | ![Advancing time by three seconds in one step using FakeTimeProvider.](https://raw.githubusercontent.com/egil/TimeProviderExtensions/main/docs/FakeTimeProvider-advance-3-seconds.svg) 220 | 221 | Technically, both implementations are correct since the `ITimer` abstractions only promise to invoke the callback timer *on or after the due time/period has elapsed*, never before. 222 | 223 | However, I strongly prefer the `ManualTimeProvider` approach since it behaves consistently independent of how time is moved forward. It seems much more in the spirit of how a deterministic time provider should behave and avoids users being surprised when writing tests. I imagine users may get stuck for a while trying to debug why the time reported by `GetUtcNow()` is not set as expected due to the subtle difference in the behavior of `FakeTimeProvider`. 224 | 225 | That said, it can be useful to test that your code behaves correctly if a timer isn't allocated processor time immediately when it's callback should fire, and for that, `ManualTimeProvider` includes a different method, `Jump`. 226 | 227 | ### Jumping to a point in time 228 | 229 | A real `ITimer`'s callback may not be allocated processor time and be able to fire at the moment it has been scheduled, e.g. if the processor is busy doing other things. The callback will eventually fire (unless the timer is disposed of). 230 | 231 | To support testing this scenario, `ManualtTimeProvider` includes a method that will jump time to a specific point, and then invoke all scheduled timer callbacks between the start and end of the jump. This behavior is similar to how `FakeTimeProvider`s `Advance` method works, as described in the previous section. 232 | 233 | ![Jumping ahead in time by three seconds in one step using ManualTimeProvider.](https://raw.githubusercontent.com/egil/TimeProviderExtensions/main/docs/jump-3-seconds.svg) 234 | -------------------------------------------------------------------------------- /TimeProviderExtensions.lutconfig: -------------------------------------------------------------------------------- 1 | 2 | 3 | true 4 | true 5 | 180000 6 | -------------------------------------------------------------------------------- /TimeProviderExtensions.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 = 10.0.40219.1 6 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{695B57CD-9E0E-4648-8A48-9E8A358B014B}" 7 | EndProject 8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{7C987338-A88E-4AB5-98CE-39A93F4CDC94}" 9 | EndProject 10 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{5DAF1353-6694-4340-BC06-28DACFB3100B}" 11 | ProjectSection(SolutionItems) = preProject 12 | .editorconfig = .editorconfig 13 | CHANGELOG.md = CHANGELOG.md 14 | src\TimeProviderExtensions\CompatibilitySuppressions.xml = src\TimeProviderExtensions\CompatibilitySuppressions.xml 15 | README.md = README.md 16 | EndProjectSection 17 | EndProject 18 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TimeProviderExtensions.Tests", "test\TimeProviderExtensions.Tests\TimeProviderExtensions.Tests.csproj", "{F475F44C-35A3-408D-824A-177EA43A8E2A}" 19 | EndProject 20 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TimeProviderExtensions", "src\TimeProviderExtensions\TimeProviderExtensions.csproj", "{E36A99D1-69FC-4418-A1EF-C9C38F8832F4}" 21 | EndProject 22 | Global 23 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 24 | Debug|Any CPU = Debug|Any CPU 25 | Release|Any CPU = Release|Any CPU 26 | EndGlobalSection 27 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 28 | {F475F44C-35A3-408D-824A-177EA43A8E2A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 29 | {F475F44C-35A3-408D-824A-177EA43A8E2A}.Debug|Any CPU.Build.0 = Debug|Any CPU 30 | {F475F44C-35A3-408D-824A-177EA43A8E2A}.Release|Any CPU.ActiveCfg = Release|Any CPU 31 | {F475F44C-35A3-408D-824A-177EA43A8E2A}.Release|Any CPU.Build.0 = Release|Any CPU 32 | {E36A99D1-69FC-4418-A1EF-C9C38F8832F4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 33 | {E36A99D1-69FC-4418-A1EF-C9C38F8832F4}.Debug|Any CPU.Build.0 = Debug|Any CPU 34 | {E36A99D1-69FC-4418-A1EF-C9C38F8832F4}.Release|Any CPU.ActiveCfg = Release|Any CPU 35 | {E36A99D1-69FC-4418-A1EF-C9C38F8832F4}.Release|Any CPU.Build.0 = Release|Any CPU 36 | EndGlobalSection 37 | GlobalSection(SolutionProperties) = preSolution 38 | HideSolutionNode = FALSE 39 | EndGlobalSection 40 | GlobalSection(NestedProjects) = preSolution 41 | {F475F44C-35A3-408D-824A-177EA43A8E2A} = {7C987338-A88E-4AB5-98CE-39A93F4CDC94} 42 | {E36A99D1-69FC-4418-A1EF-C9C38F8832F4} = {695B57CD-9E0E-4648-8A48-9E8A358B014B} 43 | EndGlobalSection 44 | GlobalSection(ExtensibilityGlobals) = postSolution 45 | SolutionGuid = {A7780397-0E8F-4C60-8D68-569022905162} 46 | EndGlobalSection 47 | EndGlobal 48 | -------------------------------------------------------------------------------- /docs/FakeTimeProvider-advance-3-seconds.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 15 | 16 | 17 | Advance(3 seconds)00:0000:0100:0200:03Timeline3xFakeTimeProvider -------------------------------------------------------------------------------- /docs/ManualTimeProvider-advance-3-seconds.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 15 | 16 | 17 | Advance(3 seconds)00:0000:0100:0200:03Timeline1x1x1xManualTimeProvider -------------------------------------------------------------------------------- /docs/System.Threading.PeriodicTimerWrapper.md: -------------------------------------------------------------------------------- 1 | #### [TimeProviderExtensions](index.md 'index') 2 | ### [System.Threading](index.md#System.Threading 'System.Threading') 3 | 4 | ## PeriodicTimerWrapper Class 5 | 6 | Provides a lightweight wrapper around a [System.Threading.PeriodicTimer](https://docs.microsoft.com/en-us/dotnet/api/System.Threading.PeriodicTimer 'System.Threading.PeriodicTimer') to enable controlling the timer via a [System.TimeProvider](https://docs.microsoft.com/en-us/dotnet/api/System.TimeProvider 'System.TimeProvider'). 7 | A periodic timer enables waiting asynchronously for timer ticks. 8 | 9 | ```csharp 10 | public abstract class PeriodicTimerWrapper : 11 | System.IDisposable 12 | ``` 13 | 14 | Inheritance [System.Object](https://docs.microsoft.com/en-us/dotnet/api/System.Object 'System.Object') 🡒 PeriodicTimerWrapper 15 | 16 | Implements [System.IDisposable](https://docs.microsoft.com/en-us/dotnet/api/System.IDisposable 'System.IDisposable') 17 | 18 | ### Remarks 19 | 20 | This timer is intended to be used only by a single consumer at a time: only one call to [WaitForNextTickAsync(CancellationToken)](System.Threading.PeriodicTimerWrapper.md#System.Threading.PeriodicTimerWrapper.WaitForNextTickAsync(System.Threading.CancellationToken) 'System.Threading.PeriodicTimerWrapper.WaitForNextTickAsync(System.Threading.CancellationToken)') 21 | may be in flight at any given moment. [Dispose()](System.Threading.PeriodicTimerWrapper.md#System.Threading.PeriodicTimerWrapper.Dispose() 'System.Threading.PeriodicTimerWrapper.Dispose()') may be used concurrently with an active [WaitForNextTickAsync(CancellationToken)](System.Threading.PeriodicTimerWrapper.md#System.Threading.PeriodicTimerWrapper.WaitForNextTickAsync(System.Threading.CancellationToken) 'System.Threading.PeriodicTimerWrapper.WaitForNextTickAsync(System.Threading.CancellationToken)') 22 | to interrupt it and cause it to return false. 23 | ### Methods 24 | 25 | 26 | 27 | ## PeriodicTimerWrapper.~PeriodicTimerWrapper() Method 28 | 29 | Ensures that resources are freed and other cleanup operations are performed when the garbage collector reclaims the [System.Threading.PeriodicTimer](https://docs.microsoft.com/en-us/dotnet/api/System.Threading.PeriodicTimer 'System.Threading.PeriodicTimer') object. 30 | 31 | ```csharp 32 | ~PeriodicTimerWrapper(); 33 | ``` 34 | 35 | 36 | 37 | ## PeriodicTimerWrapper.Dispose() Method 38 | 39 | Stops the timer and releases associated managed resources. 40 | 41 | ```csharp 42 | public void Dispose(); 43 | ``` 44 | 45 | Implements [Dispose()](https://docs.microsoft.com/en-us/dotnet/api/System.IDisposable.Dispose 'System.IDisposable.Dispose') 46 | 47 | ### Remarks 48 | [Dispose()](System.Threading.PeriodicTimerWrapper.md#System.Threading.PeriodicTimerWrapper.Dispose() 'System.Threading.PeriodicTimerWrapper.Dispose()') will cause an active wait with [WaitForNextTickAsync(CancellationToken)](System.Threading.PeriodicTimerWrapper.md#System.Threading.PeriodicTimerWrapper.WaitForNextTickAsync(System.Threading.CancellationToken) 'System.Threading.PeriodicTimerWrapper.WaitForNextTickAsync(System.Threading.CancellationToken)') to complete with a value of false. 49 | All subsequent [WaitForNextTickAsync(CancellationToken)](System.Threading.PeriodicTimerWrapper.md#System.Threading.PeriodicTimerWrapper.WaitForNextTickAsync(System.Threading.CancellationToken) 'System.Threading.PeriodicTimerWrapper.WaitForNextTickAsync(System.Threading.CancellationToken)') invocations will produce a value of false. 50 | 51 | 52 | 53 | ## PeriodicTimerWrapper.Dispose(bool) Method 54 | 55 | Dispose of the wrapped [System.Threading.PeriodicTimer](https://docs.microsoft.com/en-us/dotnet/api/System.Threading.PeriodicTimer 'System.Threading.PeriodicTimer'). 56 | 57 | ```csharp 58 | protected abstract void Dispose(bool disposing); 59 | ``` 60 | #### Parameters 61 | 62 | 63 | 64 | `disposing` [System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') 65 | 66 | 67 | 68 | ## PeriodicTimerWrapper.WaitForNextTickAsync(CancellationToken) Method 69 | 70 | Wait for the next tick of the timer, or for the timer to be stopped. 71 | 72 | ```csharp 73 | public abstract System.Threading.Tasks.ValueTask WaitForNextTickAsync(System.Threading.CancellationToken cancellationToken=default(System.Threading.CancellationToken)); 74 | ``` 75 | #### Parameters 76 | 77 | 78 | 79 | `cancellationToken` [System.Threading.CancellationToken](https://docs.microsoft.com/en-us/dotnet/api/System.Threading.CancellationToken 'System.Threading.CancellationToken') 80 | 81 | A [System.Threading.CancellationToken](https://docs.microsoft.com/en-us/dotnet/api/System.Threading.CancellationToken 'System.Threading.CancellationToken') to use to cancel the asynchronous wait. If cancellation is requested, it affects only the single wait operation; 82 | the underlying timer continues firing. 83 | 84 | #### Returns 85 | [System.Threading.Tasks.ValueTask<](https://docs.microsoft.com/en-us/dotnet/api/System.Threading.Tasks.ValueTask-1 'System.Threading.Tasks.ValueTask`1')[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean')[>](https://docs.microsoft.com/en-us/dotnet/api/System.Threading.Tasks.ValueTask-1 'System.Threading.Tasks.ValueTask`1') 86 | A task that will be completed due to the timer firing, [Dispose()](System.Threading.PeriodicTimerWrapper.md#System.Threading.PeriodicTimerWrapper.Dispose() 'System.Threading.PeriodicTimerWrapper.Dispose()') being called to stop the timer, or cancellation being requested. 87 | 88 | ### Remarks 89 | The [System.Threading.PeriodicTimer](https://docs.microsoft.com/en-us/dotnet/api/System.Threading.PeriodicTimer 'System.Threading.PeriodicTimer') behaves like an auto-reset event, in that multiple ticks are coalesced into a single tick if they occur between 90 | calls to [WaitForNextTickAsync(CancellationToken)](System.Threading.PeriodicTimerWrapper.md#System.Threading.PeriodicTimerWrapper.WaitForNextTickAsync(System.Threading.CancellationToken) 'System.Threading.PeriodicTimerWrapper.WaitForNextTickAsync(System.Threading.CancellationToken)'). Similarly, a call to [Dispose()](System.Threading.PeriodicTimerWrapper.md#System.Threading.PeriodicTimerWrapper.Dispose() 'System.Threading.PeriodicTimerWrapper.Dispose()') will void any tick not yet consumed. [WaitForNextTickAsync(CancellationToken)](System.Threading.PeriodicTimerWrapper.md#System.Threading.PeriodicTimerWrapper.WaitForNextTickAsync(System.Threading.CancellationToken) 'System.Threading.PeriodicTimerWrapper.WaitForNextTickAsync(System.Threading.CancellationToken)') 91 | may only be used by one consumer at a time, and may be used concurrently with a single call to [Dispose()](System.Threading.PeriodicTimerWrapper.md#System.Threading.PeriodicTimerWrapper.Dispose() 'System.Threading.PeriodicTimerWrapper.Dispose()'). -------------------------------------------------------------------------------- /docs/System.Threading.TimeProviderPeriodicTimerExtensions.md: -------------------------------------------------------------------------------- 1 | #### [TimeProviderExtensions](index.md 'index') 2 | ### [System.Threading](index.md#System.Threading 'System.Threading') 3 | 4 | ## TimeProviderPeriodicTimerExtensions Class 5 | 6 | PeriodicTimer extensions for [System.TimeProvider](https://docs.microsoft.com/en-us/dotnet/api/System.TimeProvider 'System.TimeProvider'). 7 | 8 | ```csharp 9 | public static class TimeProviderPeriodicTimerExtensions 10 | ``` 11 | 12 | Inheritance [System.Object](https://docs.microsoft.com/en-us/dotnet/api/System.Object 'System.Object') 🡒 TimeProviderPeriodicTimerExtensions 13 | ### Methods 14 | 15 | 16 | 17 | ## TimeProviderPeriodicTimerExtensions.CreatePeriodicTimer(this TimeProvider, TimeSpan) Method 18 | 19 | Factory method that creates a periodic timer that enables waiting asynchronously for timer ticks. 20 | Use this factory method as a replacement for instantiating a [System.Threading.PeriodicTimer](https://docs.microsoft.com/en-us/dotnet/api/System.Threading.PeriodicTimer 'System.Threading.PeriodicTimer'). 21 | 22 | ```csharp 23 | public static System.Threading.PeriodicTimerWrapper CreatePeriodicTimer(this TimeProvider timeProvider, System.TimeSpan period); 24 | ``` 25 | #### Parameters 26 | 27 | 28 | 29 | `timeProvider` [System.TimeProvider](https://docs.microsoft.com/en-us/dotnet/api/System.TimeProvider 'System.TimeProvider') 30 | 31 | 32 | 33 | `period` [System.TimeSpan](https://docs.microsoft.com/en-us/dotnet/api/System.TimeSpan 'System.TimeSpan') 34 | 35 | #### Returns 36 | [PeriodicTimerWrapper](System.Threading.PeriodicTimerWrapper.md 'System.Threading.PeriodicTimerWrapper') 37 | A new [PeriodicTimerWrapper](System.Threading.PeriodicTimerWrapper.md 'System.Threading.PeriodicTimerWrapper'). 38 | Note, this is a wrapper around a [System.Threading.PeriodicTimer](https://docs.microsoft.com/en-us/dotnet/api/System.Threading.PeriodicTimer 'System.Threading.PeriodicTimer'), 39 | and will behave exactly the same as the original. 40 | 41 | ### Remarks 42 | This timer is intended to be used only by a single consumer at a time: only one call to [System.Threading.PeriodicTimer.WaitForNextTickAsync(System.Threading.CancellationToken)](https://docs.microsoft.com/en-us/dotnet/api/System.Threading.PeriodicTimer.WaitForNextTickAsync#System_Threading_PeriodicTimer_WaitForNextTickAsync_System_Threading_CancellationToken_ 'System.Threading.PeriodicTimer.WaitForNextTickAsync(System.Threading.CancellationToken)') 43 | may be in flight at any given moment. [System.Threading.PeriodicTimer.Dispose](https://docs.microsoft.com/en-us/dotnet/api/System.Threading.PeriodicTimer.Dispose 'System.Threading.PeriodicTimer.Dispose') may be used concurrently with an active [System.Threading.PeriodicTimer.WaitForNextTickAsync(System.Threading.CancellationToken)](https://docs.microsoft.com/en-us/dotnet/api/System.Threading.PeriodicTimer.WaitForNextTickAsync#System_Threading_PeriodicTimer_WaitForNextTickAsync_System_Threading_CancellationToken_ 'System.Threading.PeriodicTimer.WaitForNextTickAsync(System.Threading.CancellationToken)') 44 | to interrupt it and cause it to return false. -------------------------------------------------------------------------------- /docs/TimeProviderExtensions.AutoAdvanceBehavior.md: -------------------------------------------------------------------------------- 1 | #### [TimeProviderExtensions](index.md 'index') 2 | ### [TimeProviderExtensions](index.md#TimeProviderExtensions 'TimeProviderExtensions') 3 | 4 | ## AutoAdvanceBehavior Class 5 | 6 | The [AutoAdvanceBehavior](TimeProviderExtensions.AutoAdvanceBehavior.md 'TimeProviderExtensions.AutoAdvanceBehavior') type provides a way to enable and customize the automatic advance of time. 7 | 8 | ```csharp 9 | public sealed class AutoAdvanceBehavior : 10 | System.IEquatable 11 | ``` 12 | 13 | Inheritance [System.Object](https://docs.microsoft.com/en-us/dotnet/api/System.Object 'System.Object') 🡒 AutoAdvanceBehavior 14 | 15 | Implements [System.IEquatable<](https://docs.microsoft.com/en-us/dotnet/api/System.IEquatable-1 'System.IEquatable`1')[AutoAdvanceBehavior](TimeProviderExtensions.AutoAdvanceBehavior.md 'TimeProviderExtensions.AutoAdvanceBehavior')[>](https://docs.microsoft.com/en-us/dotnet/api/System.IEquatable-1 'System.IEquatable`1') 16 | ### Properties 17 | 18 | 19 | 20 | ## AutoAdvanceBehavior.TimerAutoTriggerCount Property 21 | 22 | 23 | Gets or sets the amount of times timer callbacks will automatically be triggered. 24 | 25 | Setting this to a number greater than `0` causes any active timers to have their callback invoked until they have been invoked the number of times 26 | specified by [TimerAutoTriggerCount](TimeProviderExtensions.AutoAdvanceBehavior.md#TimeProviderExtensions.AutoAdvanceBehavior.TimerAutoTriggerCount 'TimeProviderExtensions.AutoAdvanceBehavior.TimerAutoTriggerCount'). Before timer callbacks are invoked, time is advanced to match 27 | the time the callback was scheduled to be invoked, just as it is if [Advance(TimeSpan)](TimeProviderExtensions.ManualTimeProvider.md#TimeProviderExtensions.ManualTimeProvider.Advance(System.TimeSpan) 'TimeProviderExtensions.ManualTimeProvider.Advance(System.TimeSpan)') 28 | or [SetUtcNow(DateTimeOffset)](TimeProviderExtensions.ManualTimeProvider.md#TimeProviderExtensions.ManualTimeProvider.SetUtcNow(System.DateTimeOffset) 'TimeProviderExtensions.ManualTimeProvider.SetUtcNow(System.DateTimeOffset)') was manually called. 29 | 30 | Setting this to `1` can be used to ensure all timers, e.g. those used by `Task.Delay(TimeSpan, TimeProvider)`, 31 | `Task.WaitAsync(TimeSpan, TimeProvider)`, `CancellationTokenSource.CancelAfter(TimeSpan)` and others 32 | are completed immediately. 33 | 34 | Setting this to a number larger than `1`, e.g. `10`, can be used to automatically cause a `PeriodicTimer(TimeSpan, TimeProvider)` 35 | to automatically have its `PeriodicTimer.WaitForNextTickAsync(CancellationToken)` async enumerable return `10` times. 36 | 37 | ```csharp 38 | public int TimerAutoTriggerCount { get; set; } 39 | ``` 40 | 41 | #### Property Value 42 | [System.Int32](https://docs.microsoft.com/en-us/dotnet/api/System.Int32 'System.Int32') 43 | 44 | #### Exceptions 45 | 46 | [System.ArgumentOutOfRangeException](https://docs.microsoft.com/en-us/dotnet/api/System.ArgumentOutOfRangeException 'System.ArgumentOutOfRangeException') 47 | Thrown when set to a value less than zero `0`. 48 | 49 | ### Remarks 50 | Set to `0` to disable auto timer callback invocation. The default value is zero `0`. 51 | 52 | 53 | 54 | ## AutoAdvanceBehavior.TimestampAdvanceAmount Property 55 | 56 | Gets or sets the amount of time by which time advances whenever the a timestamp is read via [System.TimeProvider.GetTimestamp](https://docs.microsoft.com/en-us/dotnet/api/System.TimeProvider.GetTimestamp 'System.TimeProvider.GetTimestamp') 57 | or an elapsed time is calculated with [System.TimeProvider.GetElapsedTime(System.Int64)](https://docs.microsoft.com/en-us/dotnet/api/System.TimeProvider.GetElapsedTime#System_TimeProvider_GetElapsedTime_System_Int64_ 'System.TimeProvider.GetElapsedTime(System.Int64)'). 58 | 59 | ```csharp 60 | public System.TimeSpan TimestampAdvanceAmount { get; set; } 61 | ``` 62 | 63 | #### Property Value 64 | [System.TimeSpan](https://docs.microsoft.com/en-us/dotnet/api/System.TimeSpan 'System.TimeSpan') 65 | 66 | #### Exceptions 67 | 68 | [System.ArgumentOutOfRangeException](https://docs.microsoft.com/en-us/dotnet/api/System.ArgumentOutOfRangeException 'System.ArgumentOutOfRangeException') 69 | Thrown when set to a value less than [System.TimeSpan.Zero](https://docs.microsoft.com/en-us/dotnet/api/System.TimeSpan.Zero 'System.TimeSpan.Zero'). 70 | 71 | ### Remarks 72 | Set to [System.TimeSpan.Zero](https://docs.microsoft.com/en-us/dotnet/api/System.TimeSpan.Zero 'System.TimeSpan.Zero') to disable auto advance. The default value is [System.TimeSpan.Zero](https://docs.microsoft.com/en-us/dotnet/api/System.TimeSpan.Zero 'System.TimeSpan.Zero'). 73 | 74 | 75 | 76 | ## AutoAdvanceBehavior.UtcNowAdvanceAmount Property 77 | 78 | Gets or sets the amount of time by which time advances whenever the clock is read via [System.TimeProvider.GetUtcNow](https://docs.microsoft.com/en-us/dotnet/api/System.TimeProvider.GetUtcNow 'System.TimeProvider.GetUtcNow') 79 | or [System.TimeProvider.GetLocalNow](https://docs.microsoft.com/en-us/dotnet/api/System.TimeProvider.GetLocalNow 'System.TimeProvider.GetLocalNow'). 80 | 81 | ```csharp 82 | public System.TimeSpan UtcNowAdvanceAmount { get; set; } 83 | ``` 84 | 85 | #### Property Value 86 | [System.TimeSpan](https://docs.microsoft.com/en-us/dotnet/api/System.TimeSpan 'System.TimeSpan') 87 | 88 | #### Exceptions 89 | 90 | [System.ArgumentOutOfRangeException](https://docs.microsoft.com/en-us/dotnet/api/System.ArgumentOutOfRangeException 'System.ArgumentOutOfRangeException') 91 | Thrown when set to a value less than [System.TimeSpan.Zero](https://docs.microsoft.com/en-us/dotnet/api/System.TimeSpan.Zero 'System.TimeSpan.Zero'). 92 | 93 | ### Remarks 94 | Set to [System.TimeSpan.Zero](https://docs.microsoft.com/en-us/dotnet/api/System.TimeSpan.Zero 'System.TimeSpan.Zero') to disable auto advance. The default value is [System.TimeSpan.Zero](https://docs.microsoft.com/en-us/dotnet/api/System.TimeSpan.Zero 'System.TimeSpan.Zero'). -------------------------------------------------------------------------------- /docs/TimeProviderExtensions.ManualTimer.md: -------------------------------------------------------------------------------- 1 | #### [TimeProviderExtensions](index.md 'index') 2 | ### [TimeProviderExtensions](index.md#TimeProviderExtensions 'TimeProviderExtensions') 3 | 4 | ## ManualTimer Class 5 | 6 | A implementation of a [System.Threading.ITimer](https://docs.microsoft.com/en-us/dotnet/api/System.Threading.ITimer 'System.Threading.ITimer') whose callbacks are scheduled via a [ManualTimeProvider](TimeProviderExtensions.ManualTimeProvider.md 'TimeProviderExtensions.ManualTimeProvider'). 7 | 8 | ```csharp 9 | public class ManualTimer : 10 | System.IDisposable, 11 | System.IAsyncDisposable 12 | ``` 13 | 14 | Inheritance [System.Object](https://docs.microsoft.com/en-us/dotnet/api/System.Object 'System.Object') 🡒 [System.Threading.ITimer](https://docs.microsoft.com/en-us/dotnet/api/System.Threading.ITimer 'System.Threading.ITimer') 🡒 ManualTimer 15 | 16 | Implements [System.IDisposable](https://docs.microsoft.com/en-us/dotnet/api/System.IDisposable 'System.IDisposable'), [System.IAsyncDisposable](https://docs.microsoft.com/en-us/dotnet/api/System.IAsyncDisposable 'System.IAsyncDisposable') 17 | ### Constructors 18 | 19 | 20 | 21 | ## ManualTimer(TimerCallback, object, ManualTimeProvider) Constructor 22 | 23 | Creates an instance of the [ManualTimer](TimeProviderExtensions.ManualTimer.md 'TimeProviderExtensions.ManualTimer'). No callbacks are scheduled during construction. Call [Change(TimeSpan, TimeSpan)](TimeProviderExtensions.ManualTimer.md#TimeProviderExtensions.ManualTimer.Change(System.TimeSpan,System.TimeSpan) 'TimeProviderExtensions.ManualTimer.Change(System.TimeSpan, System.TimeSpan)') to schedule invocations of [callback](TimeProviderExtensions.ManualTimer.md#TimeProviderExtensions.ManualTimer.ManualTimer(System.Threading.TimerCallback,object,TimeProviderExtensions.ManualTimeProvider).callback 'TimeProviderExtensions.ManualTimer.ManualTimer(System.Threading.TimerCallback, object, TimeProviderExtensions.ManualTimeProvider).callback') using the provided [timeProvider](TimeProviderExtensions.ManualTimer.md#TimeProviderExtensions.ManualTimer.ManualTimer(System.Threading.TimerCallback,object,TimeProviderExtensions.ManualTimeProvider).timeProvider 'TimeProviderExtensions.ManualTimer.ManualTimer(System.Threading.TimerCallback, object, TimeProviderExtensions.ManualTimeProvider).timeProvider'). 24 | 25 | ```csharp 26 | protected internal ManualTimer(System.Threading.TimerCallback callback, object? state, TimeProviderExtensions.ManualTimeProvider timeProvider); 27 | ``` 28 | #### Parameters 29 | 30 | 31 | 32 | `callback` [System.Threading.TimerCallback](https://docs.microsoft.com/en-us/dotnet/api/System.Threading.TimerCallback 'System.Threading.TimerCallback') 33 | 34 | A delegate representing a method to be executed when the timer fires. The method specified for callback should be reentrant, 35 | as it may be invoked simultaneously on two threads if the timer fires again before or while a previous callback is still being handled. 36 | 37 | 38 | 39 | `state` [System.Object](https://docs.microsoft.com/en-us/dotnet/api/System.Object 'System.Object') 40 | 41 | An object to be passed to the [callback](TimeProviderExtensions.ManualTimer.md#TimeProviderExtensions.ManualTimer.ManualTimer(System.Threading.TimerCallback,object,TimeProviderExtensions.ManualTimeProvider).callback 'TimeProviderExtensions.ManualTimer.ManualTimer(System.Threading.TimerCallback, object, TimeProviderExtensions.ManualTimeProvider).callback'). This may be null. 42 | 43 | 44 | 45 | `timeProvider` [ManualTimeProvider](TimeProviderExtensions.ManualTimeProvider.md 'TimeProviderExtensions.ManualTimeProvider') 46 | 47 | The [ManualTimeProvider](TimeProviderExtensions.ManualTimeProvider.md 'TimeProviderExtensions.ManualTimeProvider') which is used to schedule invocations of the [callback](TimeProviderExtensions.ManualTimer.md#TimeProviderExtensions.ManualTimer.ManualTimer(System.Threading.TimerCallback,object,TimeProviderExtensions.ManualTimeProvider).callback 'TimeProviderExtensions.ManualTimer.ManualTimer(System.Threading.TimerCallback, object, TimeProviderExtensions.ManualTimeProvider).callback') with. 48 | ### Properties 49 | 50 | 51 | 52 | ## ManualTimer.CallbackInvokeCount Property 53 | 54 | Gets the number of times a timer's callback has been invoked. 55 | 56 | ```csharp 57 | public int CallbackInvokeCount { get; } 58 | ``` 59 | 60 | #### Property Value 61 | [System.Int32](https://docs.microsoft.com/en-us/dotnet/api/System.Int32 'System.Int32') 62 | 63 | 64 | 65 | ## ManualTimer.CallbackTime Property 66 | 67 | Gets the next time the timer callback will be invoked, or [null](https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/null 'https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/null') if the timer is inactive. 68 | 69 | ```csharp 70 | public System.Nullable CallbackTime { get; } 71 | ``` 72 | 73 | #### Property Value 74 | [System.Nullable<](https://docs.microsoft.com/en-us/dotnet/api/System.Nullable-1 'System.Nullable`1')[System.DateTimeOffset](https://docs.microsoft.com/en-us/dotnet/api/System.DateTimeOffset 'System.DateTimeOffset')[>](https://docs.microsoft.com/en-us/dotnet/api/System.Nullable-1 'System.Nullable`1') 75 | 76 | 77 | 78 | ## ManualTimer.DueTime Property 79 | 80 | Gets the [System.TimeSpan](https://docs.microsoft.com/en-us/dotnet/api/System.TimeSpan 'System.TimeSpan') representing the amount of time to delay before invoking the callback method specified when the [System.Threading.ITimer](https://docs.microsoft.com/en-us/dotnet/api/System.Threading.ITimer 'System.Threading.ITimer') was constructed. 81 | 82 | ```csharp 83 | public System.TimeSpan DueTime { get; set; } 84 | ``` 85 | 86 | #### Property Value 87 | [System.TimeSpan](https://docs.microsoft.com/en-us/dotnet/api/System.TimeSpan 'System.TimeSpan') 88 | 89 | 90 | 91 | ## ManualTimer.IsActive Property 92 | 93 | Gets whether the timer is currently active, i.e. has a future callback invocation scheduled. 94 | 95 | ```csharp 96 | public bool IsActive { get; } 97 | ``` 98 | 99 | #### Property Value 100 | [System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') 101 | 102 | ### Remarks 103 | When [IsActive](TimeProviderExtensions.ManualTimer.md#TimeProviderExtensions.ManualTimer.IsActive 'TimeProviderExtensions.ManualTimer.IsActive') returns [true](https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/bool 'https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/bool'), [CallbackTime](TimeProviderExtensions.ManualTimer.md#TimeProviderExtensions.ManualTimer.CallbackTime 'TimeProviderExtensions.ManualTimer.CallbackTime') is not [null](https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/null 'https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/null'). 104 | 105 | 106 | 107 | ## ManualTimer.Period Property 108 | 109 | Gets the time interval between invocations of the callback method specified when the Timer was constructed. 110 | If set to [System.Threading.Timeout.InfiniteTimeSpan](https://docs.microsoft.com/en-us/dotnet/api/System.Threading.Timeout.InfiniteTimeSpan 'System.Threading.Timeout.InfiniteTimeSpan') periodic signaling is disabled. 111 | 112 | ```csharp 113 | public System.TimeSpan Period { get; set; } 114 | ``` 115 | 116 | #### Property Value 117 | [System.TimeSpan](https://docs.microsoft.com/en-us/dotnet/api/System.TimeSpan 'System.TimeSpan') 118 | ### Methods 119 | 120 | 121 | 122 | ## ManualTimer.~ManualTimer() Method 123 | 124 | The finalizer exists in case the timer is not disposed explicitly by the user. 125 | 126 | ```csharp 127 | ~ManualTimer(); 128 | ``` 129 | 130 | 131 | 132 | ## ManualTimer.Change(TimeSpan, TimeSpan) Method 133 | 134 | Changes the start time and the interval between method invocations for a timer, using [System.TimeSpan](https://docs.microsoft.com/en-us/dotnet/api/System.TimeSpan 'System.TimeSpan') values to measure time intervals. 135 | 136 | ```csharp 137 | public virtual bool Change(System.TimeSpan dueTime, System.TimeSpan period); 138 | ``` 139 | #### Parameters 140 | 141 | 142 | 143 | `dueTime` [System.TimeSpan](https://docs.microsoft.com/en-us/dotnet/api/System.TimeSpan 'System.TimeSpan') 144 | 145 | A [System.TimeSpan](https://docs.microsoft.com/en-us/dotnet/api/System.TimeSpan 'System.TimeSpan') representing the amount of time to delay before invoking the callback method specified when the [System.Threading.ITimer](https://docs.microsoft.com/en-us/dotnet/api/System.Threading.ITimer 'System.Threading.ITimer') was constructed. 146 | Specify [System.Threading.Timeout.InfiniteTimeSpan](https://docs.microsoft.com/en-us/dotnet/api/System.Threading.Timeout.InfiniteTimeSpan 'System.Threading.Timeout.InfiniteTimeSpan') to prevent the timer from restarting. Specify [System.TimeSpan.Zero](https://docs.microsoft.com/en-us/dotnet/api/System.TimeSpan.Zero 'System.TimeSpan.Zero') to restart the timer immediately. 147 | 148 | 149 | 150 | `period` [System.TimeSpan](https://docs.microsoft.com/en-us/dotnet/api/System.TimeSpan 'System.TimeSpan') 151 | 152 | The time interval between invocations of the callback method specified when the Timer was constructed. 153 | Specify [System.Threading.Timeout.InfiniteTimeSpan](https://docs.microsoft.com/en-us/dotnet/api/System.Threading.Timeout.InfiniteTimeSpan 'System.Threading.Timeout.InfiniteTimeSpan') to disable periodic signaling. 154 | 155 | #### Returns 156 | [System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') 157 | [true](https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/bool 'https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/bool') if the timer was successfully updated; otherwise, [false](https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/bool 'https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/bool'). 158 | 159 | #### Exceptions 160 | 161 | [System.ArgumentOutOfRangeException](https://docs.microsoft.com/en-us/dotnet/api/System.ArgumentOutOfRangeException 'System.ArgumentOutOfRangeException') 162 | The [dueTime](TimeProviderExtensions.ManualTimer.md#TimeProviderExtensions.ManualTimer.Change(System.TimeSpan,System.TimeSpan).dueTime 'TimeProviderExtensions.ManualTimer.Change(System.TimeSpan, System.TimeSpan).dueTime') or [period](TimeProviderExtensions.ManualTimer.md#TimeProviderExtensions.ManualTimer.Change(System.TimeSpan,System.TimeSpan).period 'TimeProviderExtensions.ManualTimer.Change(System.TimeSpan, System.TimeSpan).period') parameter, in milliseconds, is less than -1 or greater than 4294967294. 163 | 164 | ### Remarks 165 | It is the responsibility of the implementer of the ITimer interface to ensure thread safety. 166 | 167 | 168 | 169 | ## ManualTimer.Dispose() Method 170 | 171 | Disposes of the [ManualTimer](TimeProviderExtensions.ManualTimer.md 'TimeProviderExtensions.ManualTimer') and removes any scheduled callbacks from the [ManualTimeProvider](TimeProviderExtensions.ManualTimeProvider.md 'TimeProviderExtensions.ManualTimeProvider'). 172 | 173 | ```csharp 174 | public void Dispose(); 175 | ``` 176 | 177 | Implements [Dispose()](https://docs.microsoft.com/en-us/dotnet/api/System.IDisposable.Dispose 'System.IDisposable.Dispose') 178 | 179 | 180 | 181 | ## ManualTimer.Dispose(bool) Method 182 | 183 | Disposes of the [ManualTimer](TimeProviderExtensions.ManualTimer.md 'TimeProviderExtensions.ManualTimer') and removes any scheduled callbacks from the [ManualTimeProvider](TimeProviderExtensions.ManualTimeProvider.md 'TimeProviderExtensions.ManualTimeProvider'). 184 | 185 | ```csharp 186 | protected virtual void Dispose(bool disposing); 187 | ``` 188 | #### Parameters 189 | 190 | 191 | 192 | `disposing` [System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') 193 | 194 | ### Remarks 195 | If this method is overridden, it should always be called by the overriding method. 196 | 197 | 198 | 199 | ## ManualTimer.DisposeAsync() Method 200 | 201 | Disposes of the [ManualTimer](TimeProviderExtensions.ManualTimer.md 'TimeProviderExtensions.ManualTimer') and removes any scheduled callbacks from the [ManualTimeProvider](TimeProviderExtensions.ManualTimeProvider.md 'TimeProviderExtensions.ManualTimeProvider'). 202 | 203 | ```csharp 204 | public System.Threading.Tasks.ValueTask DisposeAsync(); 205 | ``` 206 | 207 | Implements [DisposeAsync()](https://docs.microsoft.com/en-us/dotnet/api/System.IAsyncDisposable.DisposeAsync 'System.IAsyncDisposable.DisposeAsync') 208 | 209 | #### Returns 210 | [System.Threading.Tasks.ValueTask](https://docs.microsoft.com/en-us/dotnet/api/System.Threading.Tasks.ValueTask 'System.Threading.Tasks.ValueTask') 211 | 212 | 213 | 214 | ## ManualTimer.ToString() Method 215 | 216 | Returns a string that represents the current object. 217 | 218 | ```csharp 219 | public override string ToString(); 220 | ``` 221 | 222 | #### Returns 223 | [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') 224 | A string that represents the current object. -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | #### [TimeProviderExtensions](index.md 'index') 2 | 3 | ## TimeProviderExtensions Assembly 4 | ### Namespaces 5 | 6 | 7 | 8 | ## System.Threading Namespace 9 | 10 | | Classes | | 11 | | :--- | :--- | 12 | | [PeriodicTimerWrapper](System.Threading.PeriodicTimerWrapper.md 'System.Threading.PeriodicTimerWrapper') | Provides a lightweight wrapper around a [System.Threading.PeriodicTimer](https://docs.microsoft.com/en-us/dotnet/api/System.Threading.PeriodicTimer 'System.Threading.PeriodicTimer') to enable controlling the timer via a [System.TimeProvider](https://docs.microsoft.com/en-us/dotnet/api/System.TimeProvider 'System.TimeProvider'). A periodic timer enables waiting asynchronously for timer ticks. | 13 | | [TimeProviderPeriodicTimerExtensions](System.Threading.TimeProviderPeriodicTimerExtensions.md 'System.Threading.TimeProviderPeriodicTimerExtensions') | PeriodicTimer extensions for [System.TimeProvider](https://docs.microsoft.com/en-us/dotnet/api/System.TimeProvider 'System.TimeProvider'). | 14 | 15 | 16 | 17 | ## TimeProviderExtensions Namespace 18 | 19 | | Classes | | 20 | | :--- | :--- | 21 | | [AutoAdvanceBehavior](TimeProviderExtensions.AutoAdvanceBehavior.md 'TimeProviderExtensions.AutoAdvanceBehavior') | The [AutoAdvanceBehavior](TimeProviderExtensions.AutoAdvanceBehavior.md 'TimeProviderExtensions.AutoAdvanceBehavior') type provides a way to enable and customize the automatic advance of time. | 22 | | [ManualTimeProvider](TimeProviderExtensions.ManualTimeProvider.md 'TimeProviderExtensions.ManualTimeProvider') | Represents a synthetic time provider that can be used to enable deterministic behavior in tests. | 23 | | [ManualTimer](TimeProviderExtensions.ManualTimer.md 'TimeProviderExtensions.ManualTimer') | A implementation of a [System.Threading.ITimer](https://docs.microsoft.com/en-us/dotnet/api/System.Threading.ITimer 'System.Threading.ITimer') whose callbacks are scheduled via a [ManualTimeProvider](TimeProviderExtensions.ManualTimeProvider.md 'TimeProviderExtensions.ManualTimeProvider'). | 24 | -------------------------------------------------------------------------------- /docs/jump-3-seconds.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 15 | 16 | 17 | Jump(3 seconds)00:0000:0100:0200:03Timeline3x -------------------------------------------------------------------------------- /key.snk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/egil/TimeProviderExtensions/7994525aa8945b4a7e3e7e10160bc3ac2e2e93b8/key.snk -------------------------------------------------------------------------------- /src/TimeProviderExtensions/AutoAdvanceBehavior.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | 3 | namespace TimeProviderExtensions; 4 | 5 | /// 6 | /// The type provides a way to enable and customize the automatic advance of time. 7 | /// 8 | public sealed record class AutoAdvanceBehavior 9 | { 10 | private TimeSpan clockAdvanceAmount = TimeSpan.Zero; 11 | private TimeSpan timestampAdvanceAmount = TimeSpan.Zero; 12 | private int timerAutoInvokeCount; 13 | 14 | /// 15 | /// Gets or sets the amount of time by which time advances whenever the clock is read via 16 | /// or . 17 | /// 18 | /// 19 | /// Set to to disable auto advance. The default value is . 20 | /// 21 | /// Thrown when set to a value less than . 22 | public TimeSpan UtcNowAdvanceAmount { get => clockAdvanceAmount; set { ThrowIfLessThanZero(value); clockAdvanceAmount = value; } } 23 | 24 | /// 25 | /// Gets or sets the amount of time by which time advances whenever the a timestamp is read via 26 | /// or an elapsed time is calculated with . 27 | /// 28 | /// 29 | /// Set to to disable auto advance. The default value is . 30 | /// 31 | /// Thrown when set to a value less than . 32 | public TimeSpan TimestampAdvanceAmount { get => timestampAdvanceAmount; set { ThrowIfLessThanZero(value); timestampAdvanceAmount = value; } } 33 | 34 | /// 35 | /// 36 | /// Gets or sets the amount of times timer callbacks will automatically be triggered. 37 | /// 38 | /// 39 | /// Setting this to a number greater than 0 causes any active timers to have their callback invoked until they have been invoked the number of times 40 | /// specified by . Before timer callbacks are invoked, time is advanced to match 41 | /// the time the callback was scheduled to be invoked, just as it is if 42 | /// or was manually called. 43 | /// 44 | /// 45 | /// Setting this to 1 can be used to ensure all timers, e.g. those used by Task.Delay(TimeSpan, TimeProvider), 46 | /// Task.WaitAsync(TimeSpan, TimeProvider), CancellationTokenSource.CancelAfter(TimeSpan) and others 47 | /// are completed immediately. 48 | /// 49 | /// 50 | /// Setting this to a number larger than 1, e.g. 10, can be used to automatically cause a PeriodicTimer(TimeSpan, TimeProvider) 51 | /// to automatically have its PeriodicTimer.WaitForNextTickAsync(CancellationToken) async enumerable return 10 times. 52 | /// 53 | /// 54 | /// 55 | /// Set to 0 to disable auto timer callback invocation. The default value is zero 0. 56 | /// 57 | /// Thrown when set to a value less than zero 0. 58 | public int TimerAutoTriggerCount { get => timerAutoInvokeCount; set { ThrowIfLessThanZero(value); timerAutoInvokeCount = value; } } 59 | 60 | private static void ThrowIfLessThanZero(TimeSpan value, [CallerMemberName] string? parameterName = null) 61 | { 62 | if (value < TimeSpan.Zero) 63 | { 64 | throw new ArgumentOutOfRangeException(parameterName, "Auto advance amounts cannot be less than zero."); 65 | } 66 | } 67 | 68 | private static void ThrowIfLessThanZero(int value, [CallerMemberName] string? parameterName = null) 69 | { 70 | if (value < 0) 71 | { 72 | throw new ArgumentOutOfRangeException(parameterName, "Auto advance amounts cannot be less than zero."); 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/TimeProviderExtensions/DefaultDocumentation.json: -------------------------------------------------------------------------------- 1 | { 2 | "Markdown.IgnoreLineBreak": true, 3 | "Markdown.DisplayAsSingleLine": true 4 | } 5 | -------------------------------------------------------------------------------- /src/TimeProviderExtensions/ManualTimer.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | using System.Diagnostics.CodeAnalysis; 3 | using System.Globalization; 4 | using System.Runtime.CompilerServices; 5 | 6 | namespace TimeProviderExtensions; 7 | 8 | /// 9 | /// A implementation of a whose callbacks are scheduled via a . 10 | /// 11 | [DebuggerDisplay("{ToString(),nq}")] 12 | public class ManualTimer : ITimer 13 | { 14 | private const uint MaxSupportedTimeout = 0xfffffffe; 15 | 16 | private readonly object lockObject = new(); 17 | private ManualTimerScheduler? scheduler; 18 | private int callbackInvokeCount; 19 | 20 | /// 21 | /// Gets whether the timer is currently active, i.e. has a future callback invocation scheduled. 22 | /// 23 | /// 24 | /// When returns , is not . 25 | /// 26 | #if NET6_0_OR_GREATER 27 | [MemberNotNullWhen(true, nameof(CallbackTime))] 28 | #endif 29 | public bool IsActive => scheduler?.CallbackTime.HasValue ?? false; 30 | 31 | /// 32 | /// Gets the next time the timer callback will be invoked, or if the timer is inactive. 33 | /// 34 | public DateTimeOffset? CallbackTime => scheduler?.CallbackTime; 35 | 36 | /// 37 | /// Gets the number of times a timer's callback has been invoked. 38 | /// 39 | public int CallbackInvokeCount => scheduler?.CallbackInvokeCount ?? callbackInvokeCount; 40 | 41 | /// 42 | /// Gets the representing the amount of time to delay before invoking the callback method specified when the was constructed. 43 | /// 44 | public TimeSpan DueTime { get; private set; } 45 | 46 | /// 47 | /// Gets the time interval between invocations of the callback method specified when the Timer was constructed. 48 | /// If set to periodic signaling is disabled. 49 | /// 50 | public TimeSpan Period { get; private set; } 51 | 52 | /// 53 | /// Creates an instance of the . No callbacks are scheduled during construction. Call to schedule invocations of using the provided . 54 | /// 55 | /// 56 | /// A delegate representing a method to be executed when the timer fires. The method specified for callback should be reentrant, 57 | /// as it may be invoked simultaneously on two threads if the timer fires again before or while a previous callback is still being handled. 58 | /// 59 | /// An object to be passed to the . This may be null. 60 | /// The which is used to schedule invocations of the with. 61 | protected internal ManualTimer(TimerCallback callback, object? state, ManualTimeProvider timeProvider) 62 | { 63 | scheduler = new ManualTimerScheduler(timeProvider, callback, state); 64 | } 65 | 66 | /// Changes the start time and the interval between method invocations for a timer, using values to measure time intervals. 67 | /// 68 | /// A representing the amount of time to delay before invoking the callback method specified when the was constructed. 69 | /// Specify to prevent the timer from restarting. Specify to restart the timer immediately. 70 | /// 71 | /// 72 | /// The time interval between invocations of the callback method specified when the Timer was constructed. 73 | /// Specify to disable periodic signaling. 74 | /// 75 | /// if the timer was successfully updated; otherwise, . 76 | /// The or parameter, in milliseconds, is less than -1 or greater than 4294967294. 77 | /// 78 | /// It is the responsibility of the implementer of the ITimer interface to ensure thread safety. 79 | /// 80 | public virtual bool Change(TimeSpan dueTime, TimeSpan period) 81 | { 82 | ValidateTimeSpanRange(dueTime); 83 | ValidateTimeSpanRange(period); 84 | 85 | lock (lockObject) 86 | { 87 | if (scheduler is null) 88 | { 89 | return false; 90 | } 91 | 92 | DueTime = dueTime; 93 | Period = period; 94 | 95 | scheduler.Change(dueTime, period); 96 | 97 | return true; 98 | } 99 | } 100 | 101 | /// 102 | public override string ToString() 103 | { 104 | if (scheduler is null) 105 | return "Timer is disposed."; 106 | 107 | var status = scheduler.CallbackTime.HasValue 108 | ? $"Next callback: {scheduler.CallbackTime.Value.ToString("yyyy-MM-ddTHH:mm:ss.fff", CultureInfo.InvariantCulture)}" 109 | : "Timer is disabled"; 110 | 111 | return $"{status}. DueTime: {(DueTime == Timeout.InfiniteTimeSpan ? "Infinite" : DueTime)}. Period: {(Period == Timeout.InfiniteTimeSpan ? "Infinite" : Period)}."; 112 | } 113 | 114 | /// 115 | /// The finalizer exists in case the timer is not disposed explicitly by the user. 116 | /// 117 | ~ManualTimer() => Dispose(false); 118 | 119 | /// 120 | /// Disposes of the and removes any scheduled callbacks from the . 121 | /// 122 | public void Dispose() 123 | { 124 | Dispose(true); 125 | GC.SuppressFinalize(this); 126 | } 127 | 128 | /// 129 | /// Disposes of the and removes any scheduled callbacks from the . 130 | /// 131 | public ValueTask DisposeAsync() 132 | { 133 | Dispose(true); 134 | GC.SuppressFinalize(this); 135 | #if NET5_0_OR_GREATER 136 | return ValueTask.CompletedTask; 137 | #else 138 | return default; 139 | #endif 140 | } 141 | 142 | /// 143 | /// Disposes of the and removes any scheduled callbacks from the . 144 | /// 145 | /// 146 | /// If this method is overridden, it should always be called by the overriding method. 147 | /// 148 | protected virtual void Dispose(bool disposing) 149 | { 150 | lock (lockObject) 151 | { 152 | if (scheduler is null) 153 | { 154 | return; 155 | } 156 | 157 | scheduler.Cancel(); 158 | callbackInvokeCount = scheduler.CallbackInvokeCount; 159 | scheduler = null; 160 | } 161 | } 162 | 163 | private static void ValidateTimeSpanRange(TimeSpan time, [CallerArgumentExpression(nameof(time))] string? parameter = null) 164 | { 165 | long tm = (long)time.TotalMilliseconds; 166 | if (tm < -1) 167 | { 168 | throw new ArgumentOutOfRangeException(parameter, $"{parameter}.TotalMilliseconds must be greater than -1."); 169 | } 170 | 171 | if (tm > MaxSupportedTimeout) 172 | { 173 | throw new ArgumentOutOfRangeException(parameter, $"{parameter}.TotalMilliseconds must be less than than {MaxSupportedTimeout}."); 174 | } 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /src/TimeProviderExtensions/ManualTimerScheduler.cs: -------------------------------------------------------------------------------- 1 | using System.Globalization; 2 | 3 | namespace TimeProviderExtensions; 4 | 5 | /// 6 | /// The type that takes care of scheduling callbacks. 7 | /// 8 | /// 9 | /// The reason this class exists and it is separate from is that 10 | /// the GC should be collect the in case users forget to dispose it. 11 | /// If all the references captured by this type was part of the 12 | /// type, the finalizer would not be invoked on if a callback was scheduled. 13 | /// 14 | internal sealed class ManualTimerScheduler 15 | { 16 | private readonly TimerCallback callback; 17 | private readonly object? state; 18 | private readonly ManualTimeProvider timeProvider; 19 | 20 | internal int CallbackInvokeCount { get; private set; } 21 | 22 | internal DateTimeOffset? CallbackTime { get; set; } 23 | 24 | internal TimeSpan Period { get; private set; } 25 | 26 | internal ManualTimerScheduler(ManualTimeProvider timeProvider, TimerCallback callback, object? state) 27 | { 28 | this.timeProvider = timeProvider; 29 | this.callback = callback; 30 | this.state = state; 31 | } 32 | 33 | internal void Cancel() 34 | { 35 | if (CallbackTime.HasValue) 36 | { 37 | CallbackTime = null; 38 | timeProvider.RemoveCallback(this); 39 | } 40 | } 41 | 42 | internal void Change(TimeSpan dueTime, TimeSpan period) 43 | { 44 | Cancel(); 45 | 46 | Period = period; 47 | 48 | if (dueTime != Timeout.InfiniteTimeSpan) 49 | { 50 | ScheduleCallback(dueTime); 51 | } 52 | } 53 | 54 | internal void TimerElapsed(bool scheduleNextCallback) 55 | { 56 | CallbackTime = null; 57 | callback.Invoke(state); 58 | CallbackInvokeCount++; 59 | 60 | if (scheduleNextCallback && Period != Timeout.InfiniteTimeSpan && Period != TimeSpan.Zero) 61 | { 62 | ScheduleCallback(Period); 63 | } 64 | } 65 | 66 | private void ScheduleCallback(TimeSpan waitTime) 67 | { 68 | if (waitTime == TimeSpan.Zero) 69 | { 70 | TimerElapsed(scheduleNextCallback: true); 71 | } 72 | else 73 | { 74 | timeProvider.ScheduleCallback(this, waitTime); 75 | } 76 | } 77 | } -------------------------------------------------------------------------------- /src/TimeProviderExtensions/System.Runtime.CompilerServices/CallerArgumentExpressionAttribute.cs: -------------------------------------------------------------------------------- 1 | #if !NET6_0_OR_GREATER 2 | using System.Diagnostics.CodeAnalysis; 3 | 4 | namespace System.Runtime.CompilerServices; 5 | 6 | [ExcludeFromCodeCoverage] 7 | [AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = false)] 8 | internal sealed class CallerArgumentExpressionAttribute : Attribute 9 | { 10 | public CallerArgumentExpressionAttribute(string parameterName) 11 | { 12 | ParameterName = parameterName; 13 | } 14 | 15 | public string ParameterName { get; } 16 | } 17 | #endif -------------------------------------------------------------------------------- /src/TimeProviderExtensions/System.Runtime.CompilerServices/IsExternalInit.cs: -------------------------------------------------------------------------------- 1 | #if NETSTANDARD2_0 2 | // Licensed to the .NET Foundation under one or more agreements. 3 | // The .NET Foundation licenses this file to you under the MIT license. 4 | 5 | using System.ComponentModel; 6 | using System.Diagnostics.CodeAnalysis; 7 | 8 | namespace System.Runtime.CompilerServices 9 | { 10 | /// 11 | /// Reserved to be used by the compiler for tracking metadata. 12 | /// This class should not be used by developers in source code. 13 | /// This dummy class is required to compile records when targeting .NET Standard 14 | /// 15 | [ExcludeFromCodeCoverage] 16 | [EditorBrowsable(EditorBrowsableState.Never)] 17 | internal static class IsExternalInit 18 | { 19 | } 20 | } 21 | #endif -------------------------------------------------------------------------------- /src/TimeProviderExtensions/System.Threading/PeriodicTimerPort.cs: -------------------------------------------------------------------------------- 1 | // The code in this file is based on the following source, 2 | // which is licensed to the .NET Foundation under one or more agreements. 3 | // Original code: https://github.com/dotnet/runtime/blob/0096ba52e8c86e4d712013f6330a9b8a6496a1e0/src/libraries/System.Private.CoreLib/src/System/Threading/PeriodicTimer.cs 4 | 5 | #if NET6_0_OR_GREATER && !NET8_0_OR_GREATER 6 | using System; 7 | using System.Diagnostics; 8 | using System.Diagnostics.CodeAnalysis; 9 | using System.Runtime.ExceptionServices; 10 | using System.Threading.Tasks; 11 | using System.Threading.Tasks.Sources; 12 | 13 | namespace TimeProviderExtensions; 14 | 15 | /// Provides a periodic timer that enables waiting asynchronously for timer ticks. 16 | /// 17 | /// This timer is intended to be used only by a single consumer at a time: only one call to 18 | /// may be in flight at any given moment. may be used concurrently with an active 19 | /// to interrupt it and cause it to return false. 20 | /// 21 | [ExcludeFromCodeCoverage] 22 | internal sealed class PeriodicTimerPort : IDisposable 23 | { 24 | internal const uint MaxSupportedTimeout = 0xfffffffe; 25 | internal const uint UnsignedInfinite = unchecked((uint)-1); 26 | 27 | /// The underlying timer. 28 | private readonly ITimer _timer; 29 | /// All state other than the _timer, so that the rooted timer's callback doesn't indirectly root itself by referring to _timer. 30 | private readonly State _state; 31 | 32 | /// Initializes the timer. 33 | /// The period between ticks 34 | /// The used to interpret . 35 | /// must be or represent a number of milliseconds equal to or larger than 1 and smaller than . 36 | /// is null 37 | internal PeriodicTimerPort(TimeSpan period, TimeProvider timeProvider) 38 | { 39 | if (!TryGetMilliseconds(period, out uint ms)) 40 | { 41 | GC.SuppressFinalize(this); 42 | throw new ArgumentOutOfRangeException(nameof(period)); 43 | } 44 | 45 | if (timeProvider is null) 46 | { 47 | GC.SuppressFinalize(this); 48 | throw new ArgumentNullException(nameof(timeProvider)); 49 | } 50 | 51 | _state = new State(); 52 | TimerCallback callback = s => ((State)s!).Signal(); 53 | 54 | using (ExecutionContext.SuppressFlow()) 55 | { 56 | _timer = timeProvider.CreateTimer(callback, _state, period, period); 57 | } 58 | } 59 | 60 | /// Tries to extract the number of milliseconds from . 61 | /// 62 | /// true if the number of milliseconds is extracted and stored into ; 63 | /// false if the number of milliseconds would be out of range of a timer. 64 | /// 65 | private static bool TryGetMilliseconds(TimeSpan value, out uint milliseconds) 66 | { 67 | long ms = (long)value.TotalMilliseconds; 68 | if ((ms >= 1 && ms <= MaxSupportedTimeout) || value == Timeout.InfiniteTimeSpan) 69 | { 70 | milliseconds = (uint)ms; 71 | return true; 72 | } 73 | 74 | milliseconds = 0; 75 | return false; 76 | } 77 | 78 | /// Wait for the next tick of the timer, or for the timer to be stopped. 79 | /// 80 | /// A to use to cancel the asynchronous wait. If cancellation is requested, it affects only the single wait operation; 81 | /// the underlying timer continues firing. 82 | /// 83 | /// A task that will be completed due to the timer firing, being called to stop the timer, or cancellation being requested. 84 | /// 85 | /// The behaves like an auto-reset event, in that multiple ticks are coalesced into a single tick if they occur between 86 | /// calls to . Similarly, a call to will void any tick not yet consumed. 87 | /// may only be used by one consumer at a time, and may be used concurrently with a single call to . 88 | /// 89 | public ValueTask WaitForNextTickAsync(CancellationToken cancellationToken = default) => 90 | _state.WaitForNextTickAsync(this, cancellationToken); 91 | 92 | /// Stops the timer and releases associated managed resources. 93 | /// 94 | /// will cause an active wait with to complete with a value of false. 95 | /// All subsequent invocations will produce a value of false. 96 | /// 97 | public void Dispose() 98 | { 99 | GC.SuppressFinalize(this); 100 | _timer.Dispose(); 101 | _state.Signal(stopping: true); 102 | } 103 | 104 | /// Ensures that resources are freed and other cleanup operations are performed when the garbage collector reclaims the object. 105 | ~PeriodicTimerPort() => Dispose(); 106 | 107 | /// Core implementation for the periodic timer. 108 | [SuppressMessage("Reliability", "CA2002:Do not lock on objects with weak identity", Justification = "Code copied from Microsoft.")] 109 | private sealed class State : IValueTaskSource 110 | { 111 | /// The associated . 112 | /// 113 | /// This should refer to the parent instance only when there's an active waiter, and be null when there 114 | /// isn't. The TimerQueueTimer in the PeriodicTimer strongly roots itself, and it references this State 115 | /// object: 116 | /// PeriodicTimer (finalizable) --ref--> TimerQueueTimer (rooted) --ref--> State --ref--> null 117 | /// If this State object then references the PeriodicTimer, it creates a strongly-rooted cycle that prevents anything from 118 | /// being GC'd: 119 | /// PeriodicTimer (finalizable) --ref--> TimerQueueTimer (rooted) --ref--> State --v 120 | /// ^--ref-------------------------------------------------------------------| 121 | /// When this field is null, the cycle is broken, and dropping all references to the PeriodicTimer allows the 122 | /// PeriodicTimer to be finalized and unroot the TimerQueueTimer. Thus, we keep this field set during 123 | /// so that the timer roots any async continuation chain awaiting it, and then keep it unset otherwise so that everything 124 | /// can be GC'd appropriately. 125 | /// 126 | /// Note that if the period is set to infinite, even when there's an active waiter the PeriodicTimer won't 127 | /// be rooted because TimerQueueTimer won't be rooted via the static linked list. That's fine, as the timer 128 | /// will never tick in such a case, and for the timer's period to be changed, the user's code would need 129 | /// some other reference to PeriodicTimer keeping it alive, anyway. 130 | /// 131 | private PeriodicTimerPort? _owner; 132 | /// Core of the implementation. 133 | private ManualResetValueTaskSourceCore _mrvtsc; 134 | /// Cancellation registration for any active call. 135 | private CancellationTokenRegistration _ctr; 136 | /// Whether the timer has been stopped. 137 | private bool _stopped; 138 | /// Whether there's a pending notification to be received. This could be due to the timer firing, the timer being stopped, or cancellation being requested. 139 | private bool _signaled; 140 | /// Whether there's a call in flight. 141 | private bool _activeWait; 142 | 143 | /// Wait for the next tick of the timer, or for the timer to be stopped. 144 | public ValueTask WaitForNextTickAsync(PeriodicTimerPort owner, CancellationToken cancellationToken) 145 | { 146 | lock (this) 147 | { 148 | if (_activeWait) 149 | { 150 | throw new InvalidOperationException("WaitForNextTickAsync should only be used by one consumer at a time. Failing to do so is an error."); 151 | } 152 | 153 | // If cancellation has already been requested, short-circuit. 154 | if (cancellationToken.IsCancellationRequested) 155 | { 156 | return ValueTask.FromCanceled(cancellationToken); 157 | } 158 | 159 | // If the timer has a pending tick or has been stopped, we can complete synchronously. 160 | if (_signaled) 161 | { 162 | // Reset the signal for subsequent consumers, but only if we're not stopped. Since. 163 | // stopping the timer is one way, any subsequent calls should also complete synchronously 164 | // with false, and thus we leave _signaled pinned at true. 165 | if (!_stopped) 166 | { 167 | _signaled = false; 168 | } 169 | 170 | return new ValueTask(!_stopped); 171 | } 172 | 173 | Debug.Assert(!_stopped, "Unexpectedly stopped without _signaled being true."); 174 | 175 | // Set up for the wait and return a task that will be signaled when the 176 | // timer fires, stop is called, or cancellation is requested. 177 | _owner = owner; 178 | _activeWait = true; 179 | _ctr = cancellationToken.UnsafeRegister(static (state, cancellationToken) => ((State)state!).Signal(cancellationToken: cancellationToken), this); 180 | 181 | return new ValueTask(this, _mrvtsc.Version); 182 | } 183 | } 184 | 185 | /// Signal that the timer has either fired or been stopped. 186 | public void Signal(bool stopping = false, CancellationToken cancellationToken = default) 187 | { 188 | bool completeTask = false; 189 | 190 | lock (this) 191 | { 192 | _stopped |= stopping; 193 | if (!_signaled) 194 | { 195 | _signaled = true; 196 | completeTask = _activeWait; 197 | } 198 | } 199 | 200 | if (completeTask) 201 | { 202 | if (cancellationToken.IsCancellationRequested) 203 | { 204 | // If cancellation is requested just before the UnsafeRegister call, it's possible this will end up being invoked 205 | // as part of the WaitForNextTickAsync call and thus as part of holding the lock. The goal of completeTask 206 | // was to escape that lock, so that we don't invoke any synchronous continuations from the ValueTask as part 207 | // of completing _mrvtsc. However, in that case, we also haven't returned the ValueTask to the caller, so there 208 | // won't be any continuations yet, which makes this safe. 209 | _mrvtsc.SetException(ExceptionDispatchInfo.SetCurrentStackTrace(new OperationCanceledException(cancellationToken))); 210 | } 211 | else 212 | { 213 | Debug.Assert(!Monitor.IsEntered(this)); 214 | _mrvtsc.SetResult(true); 215 | } 216 | } 217 | } 218 | 219 | /// 220 | bool IValueTaskSource.GetResult(short token) 221 | { 222 | // Dispose of the cancellation registration. This is done outside of the below lock in order 223 | // to avoid a potential deadlock due to waiting for a concurrent cancellation callback that might 224 | // in turn try to take the lock. For valid usage, GetResult is only called once _ctr has been 225 | // successfully initialized before WaitForNextTickAsync returns to its synchronous caller, and 226 | // there should be no race conditions accessing it, as concurrent consumption is invalid. If there 227 | // is invalid usage, with GetResult used erroneously/concurrently, the worst that happens is cancellation 228 | // may not take effect for the in-flight operation, with its registration erroneously disposed. 229 | // Note we use Dispose rather than Unregister (which wouldn't risk deadlock) so that we know that thecancellation callback associated with this operation 230 | // won't potentially still fire after we've completed this GetResult and a new operation 231 | // has potentially started. 232 | _ctr.Dispose(); 233 | 234 | lock (this) 235 | { 236 | try 237 | { 238 | _mrvtsc.GetResult(token); 239 | } 240 | finally 241 | { 242 | _mrvtsc.Reset(); 243 | _ctr = default; 244 | _activeWait = false; 245 | _owner = null; 246 | if (!_stopped) 247 | { 248 | _signaled = false; 249 | } 250 | } 251 | 252 | return !_stopped; 253 | } 254 | } 255 | 256 | /// 257 | ValueTaskSourceStatus IValueTaskSource.GetStatus(short token) => _mrvtsc.GetStatus(token); 258 | 259 | /// 260 | void IValueTaskSource.OnCompleted(Action continuation, object? state, short token, ValueTaskSourceOnCompletedFlags flags) => 261 | _mrvtsc.OnCompleted(continuation, state, token, flags); 262 | } 263 | } 264 | #endif 265 | -------------------------------------------------------------------------------- /src/TimeProviderExtensions/System.Threading/PeriodicTimerWrapper.cs: -------------------------------------------------------------------------------- 1 | #if NET6_0_OR_GREATER && !NET8_0_OR_GREATER 2 | using System.Diagnostics.CodeAnalysis; 3 | using TimeProviderExtensions; 4 | 5 | namespace System.Threading; 6 | 7 | /// 8 | /// Provides a lightweight wrapper around a to enable controlling the timer via a . 9 | /// A periodic timer enables waiting asynchronously for timer ticks. 10 | /// 11 | /// 12 | /// 13 | /// This timer is intended to be used only by a single consumer at a time: only one call to 14 | /// may be in flight at any given moment. may be used concurrently with an active 15 | /// to interrupt it and cause it to return false. 16 | /// 17 | /// 18 | [ExcludeFromCodeCoverage] 19 | public abstract class PeriodicTimerWrapper : IDisposable 20 | { 21 | /// Wait for the next tick of the timer, or for the timer to be stopped. 22 | /// 23 | /// A to use to cancel the asynchronous wait. If cancellation is requested, it affects only the single wait operation; 24 | /// the underlying timer continues firing. 25 | /// 26 | /// A task that will be completed due to the timer firing, being called to stop the timer, or cancellation being requested. 27 | /// 28 | /// The behaves like an auto-reset event, in that multiple ticks are coalesced into a single tick if they occur between 29 | /// calls to . Similarly, a call to will void any tick not yet consumed. 30 | /// may only be used by one consumer at a time, and may be used concurrently with a single call to . 31 | /// 32 | public abstract ValueTask WaitForNextTickAsync(CancellationToken cancellationToken = default); 33 | 34 | /// Stops the timer and releases associated managed resources. 35 | /// 36 | /// will cause an active wait with to complete with a value of false. 37 | /// All subsequent invocations will produce a value of false. 38 | /// 39 | public void Dispose() 40 | { 41 | Dispose(true); 42 | GC.SuppressFinalize(this); 43 | } 44 | 45 | /// Ensures that resources are freed and other cleanup operations are performed when the garbage collector reclaims the object. 46 | ~PeriodicTimerWrapper() => Dispose(false); 47 | 48 | /// Dispose of the wrapped . 49 | protected abstract void Dispose(bool disposing); 50 | 51 | internal sealed class PeriodicTimerPortWrapper : PeriodicTimerWrapper 52 | { 53 | private readonly PeriodicTimerPort periodicTimer; 54 | 55 | public PeriodicTimerPortWrapper(PeriodicTimerPort periodicTimer) 56 | { 57 | this.periodicTimer = periodicTimer; 58 | } 59 | 60 | protected override void Dispose(bool disposing) 61 | => periodicTimer.Dispose(); 62 | 63 | public override ValueTask WaitForNextTickAsync(CancellationToken cancellationToken = default) 64 | => periodicTimer.WaitForNextTickAsync(cancellationToken); 65 | } 66 | 67 | internal sealed class PeriodicTimerOrgWrapper : PeriodicTimerWrapper 68 | { 69 | private readonly PeriodicTimer periodicTimer; 70 | 71 | public PeriodicTimerOrgWrapper(PeriodicTimer periodicTimer) 72 | { 73 | this.periodicTimer = periodicTimer; 74 | } 75 | 76 | protected override void Dispose(bool disposing) 77 | => periodicTimer.Dispose(); 78 | 79 | public override ValueTask WaitForNextTickAsync(CancellationToken cancellationToken = default) 80 | => periodicTimer.WaitForNextTickAsync(cancellationToken); 81 | } 82 | } 83 | #endif -------------------------------------------------------------------------------- /src/TimeProviderExtensions/System.Threading/TimeProviderPeriodicTimerExtensions.cs: -------------------------------------------------------------------------------- 1 | using TimeProviderExtensions; 2 | 3 | namespace System.Threading; 4 | 5 | /// 6 | /// PeriodicTimer extensions for . 7 | /// 8 | public static class TimeProviderPeriodicTimerExtensions 9 | { 10 | #if NET6_0_OR_GREATER && !NET8_0_OR_GREATER 11 | /// 12 | /// Factory method that creates a periodic timer that enables waiting asynchronously for timer ticks. 13 | /// Use this factory method as a replacement for instantiating a . 14 | /// 15 | /// 16 | /// This timer is intended to be used only by a single consumer at a time: only one call to 17 | /// may be in flight at any given moment. may be used concurrently with an active 18 | /// to interrupt it and cause it to return false. 19 | /// 20 | /// 21 | /// A new . 22 | /// Note, this is a wrapper around a , 23 | /// and will behave exactly the same as the original. 24 | /// 25 | public static PeriodicTimerWrapper CreatePeriodicTimer(this TimeProvider timeProvider, TimeSpan period) 26 | { 27 | ArgumentNullException.ThrowIfNull(timeProvider); 28 | return timeProvider == TimeProvider.System 29 | ? new PeriodicTimerWrapper.PeriodicTimerOrgWrapper(new PeriodicTimer(period)) 30 | : new PeriodicTimerWrapper.PeriodicTimerPortWrapper(new PeriodicTimerPort(period, timeProvider)); 31 | } 32 | #endif 33 | #if NET8_0_OR_GREATER 34 | /// 35 | /// Factory method that creates a periodic timer that enables waiting asynchronously for timer ticks. 36 | /// Use this factory method as a replacement for instantiating a . 37 | /// 38 | /// 39 | /// This timer is intended to be used only by a single consumer at a time: only one call to 40 | /// may be in flight at any given moment. may be used concurrently with an active 41 | /// to interrupt it and cause it to return false. 42 | /// 43 | /// A new . 44 | public static System.Threading.PeriodicTimer CreatePeriodicTimer(this TimeProvider timeProvider, TimeSpan period) 45 | => new PeriodicTimer(period, timeProvider); 46 | #endif 47 | } -------------------------------------------------------------------------------- /src/TimeProviderExtensions/TimeProviderExtensions.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | TimeProviderExtensions 5 | TimeProvider Extensions 6 | Egil Hansen 7 | Egil Hansen 8 | 9 | Testing extensions for the System.TimeProvider API. It includes 10 | an advanced test/fake version of the TimeProvider type that allows you to control the progress of time 11 | during testing deterministically and a backported version of PeriodicTimer that supports TimeProvider in .NET 6. 12 | 13 | README.md 14 | TimeProvider, testing 15 | Egil Hansen 16 | https://github.com/egil/TimeProviderExtensions 17 | https://github.com/egil/TimeProviderExtensions 18 | git 19 | LICENSE 20 | true 21 | true 22 | v 23 | true 24 | true 25 | 1.0.0 26 | 27 | 28 | 29 | 30 | $([System.IO.File]::ReadAllText("$(MSBuildProjectDirectory)/../../CHANGELOG.md")) 31 | 32 | 33 | 34 | 35 | net8.0;net6.0;netstandard2.0 36 | enable 37 | enable 38 | true 39 | preview 40 | enable 41 | true 42 | embedded 43 | true 44 | ../../key.snk 45 | 46 | 47 | 48 | true 49 | ../../docs 50 | Public,Protected,ProtectedInternal 51 | Classes 52 | 53 | 54 | 55 | AllEnabledByDefault 56 | true 57 | true 58 | True 59 | latest-all 60 | 61 | 62 | 63 | 64 | 65 | runtime; build; native; contentfiles; analyzers; buildtransitive 66 | all 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | True 75 | \ 76 | 77 | 78 | True 79 | \ 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | -------------------------------------------------------------------------------- /stryker-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "stryker-config": { 3 | "mutate": [ 4 | "!**/System.*/*" 5 | ], 6 | "language-version": "Preview", 7 | "target-framework": "net8.0", 8 | "mutation-level": "Complete", 9 | "reporters": [ 10 | "markdown", 11 | "dashboard", 12 | "html" 13 | ], 14 | "baseline": { 15 | "enabled": false, 16 | "fallback-version": "main", 17 | "provider": "Dashboard" 18 | }, 19 | "thresholds": { 20 | "high": 80, 21 | "low": 60, 22 | "break": 60 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /test/TimeProviderExtensions.Tests/AutoAdvanceBehaviorTests.cs: -------------------------------------------------------------------------------- 1 | namespace TimeProviderExtensions; 2 | 3 | public class AutoAdvanceBehaviorTests 4 | { 5 | [Fact] 6 | public void ClockAdvanceAmount_throws_when_lt_zero() 7 | { 8 | var sut = new AutoAdvanceBehavior(); 9 | 10 | var throws = () => sut.UtcNowAdvanceAmount = TimeSpan.FromTicks(-1); 11 | 12 | throws.Should().Throw() 13 | .And.ParamName.Should().Be(nameof(AutoAdvanceBehavior.UtcNowAdvanceAmount)); 14 | } 15 | 16 | [Fact] 17 | public void TimestampAdvanceAmount_throws_when_lt_zero() 18 | { 19 | var sut = new AutoAdvanceBehavior(); 20 | 21 | var throws = () => sut.TimestampAdvanceAmount = TimeSpan.FromTicks(-1); 22 | 23 | throws.Should().Throw() 24 | .And.ParamName.Should().Be(nameof(AutoAdvanceBehavior.TimestampAdvanceAmount)); 25 | } 26 | 27 | [Fact] 28 | public void TimerAutoTriggerCount_throws_when_lt_zero() 29 | { 30 | var sut = new AutoAdvanceBehavior(); 31 | 32 | var throws = () => sut.TimerAutoTriggerCount = -1; 33 | 34 | throws.Should().Throw() 35 | .And.ParamName.Should().Be(nameof(AutoAdvanceBehavior.TimerAutoTriggerCount)); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /test/TimeProviderExtensions.Tests/FluentAssertions/TaskAssertionsExtensions.cs: -------------------------------------------------------------------------------- 1 | using FluentAssertions.Primitives; 2 | 3 | namespace FluentAssertions; 4 | 5 | internal static class TaskAssertionsExtensions 6 | { 7 | public static TaskAssertions Should(this Task subject) 8 | { 9 | return new TaskAssertions(subject); 10 | } 11 | } 12 | 13 | public class TaskAssertions : ReferenceTypeAssertions 14 | { 15 | public TaskAssertions(Task subject) : base(subject) 16 | { 17 | } 18 | 19 | protected override string Identifier => "task"; 20 | 21 | public AndConstraint CompletedSuccessfully(string because = "", params object[] becauseArgs) 22 | { 23 | #if NET6_0_OR_GREATER 24 | Subject.IsCompletedSuccessfully.Should().BeTrue(because, becauseArgs); 25 | #else 26 | Subject.IsCompleted.Should().BeTrue(because, becauseArgs); 27 | Subject.IsFaulted.Should().BeFalse(because, becauseArgs); 28 | #endif 29 | return new AndConstraint(this); 30 | } 31 | 32 | public AndConstraint Canceled(string because = "", params object[] becauseArgs) 33 | { 34 | Subject.IsCanceled.Should().BeTrue(because, becauseArgs); 35 | return new AndConstraint(this); 36 | } 37 | 38 | 39 | public async Task> CompletedSuccessfullyAsync(string because = "", params object[] becauseArgs) 40 | { 41 | await Subject; 42 | return CompletedSuccessfully(because, becauseArgs); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /test/TimeProviderExtensions.Tests/ManualTimeProviderCancelAfter.cs.cs: -------------------------------------------------------------------------------- 1 | namespace TimeProviderExtensions; 2 | 3 | public class ManualTimeProviderCancelAfter 4 | { 5 | [Fact] 6 | public void CancelAfter_cancels() 7 | { 8 | var delay = TimeSpan.FromMilliseconds(42); 9 | var sut = new ManualTimeProvider(); 10 | using var cts = sut.CreateCancellationTokenSource(delay); 11 | 12 | sut.Advance(delay); 13 | 14 | cts.IsCancellationRequested.Should().BeTrue(); 15 | } 16 | 17 | #if NET8_0_OR_GREATER 18 | // The following two scenarios are only supported by .NET 8 and up. 19 | [Fact] 20 | public void CancelAfter_reschedule_longer_cancel() 21 | { 22 | var initialDelay = TimeSpan.FromMilliseconds(100); 23 | var rescheduledDelay = TimeSpan.FromMilliseconds(1000); 24 | var sut = new ManualTimeProvider(); 25 | using var cts = sut.CreateCancellationTokenSource(initialDelay); 26 | 27 | cts.CancelAfter(rescheduledDelay); 28 | 29 | sut.Advance(initialDelay); 30 | cts.IsCancellationRequested.Should().BeFalse(); 31 | 32 | sut.Advance(rescheduledDelay - initialDelay); 33 | cts.IsCancellationRequested.Should().BeTrue(); 34 | } 35 | 36 | [Fact] 37 | public async void CancelAfter_reschedule_shorter_cancel() 38 | { 39 | var initialDelay = TimeSpan.FromMilliseconds(1000); 40 | var rescheduledDelay = TimeSpan.FromMilliseconds(100); 41 | var sut = new ManualTimeProvider(); 42 | using var cts = sut.CreateCancellationTokenSource(initialDelay); 43 | 44 | cts.CancelAfter(rescheduledDelay); 45 | 46 | sut.Advance(rescheduledDelay); 47 | await Task.Delay(1000); 48 | 49 | cts.IsCancellationRequested.Should().BeTrue(); 50 | } 51 | #endif 52 | } 53 | -------------------------------------------------------------------------------- /test/TimeProviderExtensions.Tests/ManualTimeProviderDelayTests.cs: -------------------------------------------------------------------------------- 1 | namespace TimeProviderExtensions; 2 | 3 | public class ManualTimeProviderDelayTests 4 | { 5 | [Fact] 6 | public void Delayed_task_is_completes() 7 | { 8 | var startTime = DateTimeOffset.UtcNow; 9 | var future = TimeSpan.FromMilliseconds(1); 10 | var sut = new ManualTimeProvider(startTime); 11 | var task = sut.Delay(TimeSpan.FromMilliseconds(1)); 12 | 13 | sut.Advance(future); 14 | 15 | task.Status.Should().Be(TaskStatus.RanToCompletion); 16 | } 17 | 18 | #if NET8_0_OR_GREATER 19 | [Fact] 20 | #else 21 | [Fact(Skip = "Bug in .NET 7 and earlier - https://github.com/dotnet/runtime/issues/92264")] 22 | #endif 23 | public void Delayed_task_is_cancelled() 24 | { 25 | using var cts = new CancellationTokenSource(); 26 | var sut = new ManualTimeProvider(); 27 | var task = sut.Delay(TimeSpan.FromMilliseconds(1), cts.Token); 28 | 29 | cts.Cancel(); 30 | 31 | task.Status.Should().Be(TaskStatus.Canceled); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /test/TimeProviderExtensions.Tests/ManualTimeProviderPeriodicTimerTests.cs: -------------------------------------------------------------------------------- 1 | #if NET6_0_OR_GREATER 2 | 3 | namespace TimeProviderExtensions; 4 | 5 | public class ManualTimeProviderPeriodicTimerTests 6 | { 7 | [Fact] 8 | public void PeriodicTimer_WaitForNextTickAsync_cancelled_immediately() 9 | { 10 | using var cts = new CancellationTokenSource(); 11 | var sut = new ManualTimeProvider(); 12 | using var periodicTimer = sut.CreatePeriodicTimer(TimeSpan.FromMilliseconds(1)); 13 | 14 | cts.Cancel(); 15 | var task = periodicTimer.WaitForNextTickAsync(cts.Token); 16 | 17 | task.IsCanceled.Should().BeTrue(); 18 | } 19 | 20 | [Fact] 21 | public async Task PeriodicTimer_WaitForNextTickAsync_complete_immediately() 22 | { 23 | var sut = new ManualTimeProvider(); 24 | using var periodicTimer = sut.CreatePeriodicTimer(TimeSpan.FromMilliseconds(1)); 25 | 26 | sut.Advance(TimeSpan.FromMilliseconds(1)); 27 | var task = periodicTimer.WaitForNextTickAsync(); 28 | 29 | (await task).Should().BeTrue(); 30 | } 31 | 32 | [Fact] 33 | public async Task PeriodicTimer_WaitForNextTickAsync_completes() 34 | { 35 | var startTime = DateTimeOffset.UtcNow; 36 | var future = TimeSpan.FromMilliseconds(1); 37 | var sut = new ManualTimeProvider(startTime); 38 | using var periodicTimer = sut.CreatePeriodicTimer(TimeSpan.FromMilliseconds(1)); 39 | var task = periodicTimer.WaitForNextTickAsync(); 40 | 41 | sut.Advance(future); 42 | 43 | (await task).Should().BeTrue(); 44 | } 45 | 46 | [Fact] 47 | public async Task PeriodicTimer_WaitForNextTickAsync_completes_after_dispose() 48 | { 49 | var startTime = DateTimeOffset.UtcNow; 50 | var sut = new ManualTimeProvider(startTime); 51 | var periodicTimer = sut.CreatePeriodicTimer(TimeSpan.FromMilliseconds(1)); 52 | var task = periodicTimer.WaitForNextTickAsync(); 53 | 54 | periodicTimer.Dispose(); 55 | 56 | (await task).Should().BeFalse(); 57 | } 58 | 59 | [Fact] 60 | public async Task PeriodicTimer_WaitForNextTickAsync_cancelled_with_exception() 61 | { 62 | using var cts = new CancellationTokenSource(); 63 | var sut = new ManualTimeProvider(); 64 | using var periodicTimer = sut.CreatePeriodicTimer(TimeSpan.FromMilliseconds(1)); 65 | var task = periodicTimer.WaitForNextTickAsync(cts.Token); 66 | cts.Cancel(); 67 | 68 | var throws = async () => await task; 69 | 70 | await throws 71 | .Should() 72 | .ThrowAsync(); 73 | } 74 | 75 | [Fact] 76 | public void PeriodicTimer_WaitForNextTickAsync_twice_throws() 77 | { 78 | var sut = new ManualTimeProvider(); 79 | using var periodicTimer = sut.CreatePeriodicTimer(TimeSpan.FromMilliseconds(1)); 80 | 81 | _ = periodicTimer.WaitForNextTickAsync(); 82 | var throws = () => periodicTimer.WaitForNextTickAsync(); 83 | 84 | throws.Should().ThrowExactly(); 85 | } 86 | 87 | [Fact] 88 | public void PeriodicTimer_WaitForNextTickAsync_completes_multiple() 89 | { 90 | var sut = new ManualTimeProvider(); 91 | var calledTimes = 0; 92 | var interval = TimeSpan.FromSeconds(1); 93 | var looper = WaitForNextTickInLoop(sut, () => calledTimes++, interval); 94 | 95 | sut.Advance(interval); 96 | calledTimes.Should().Be(1); 97 | 98 | sut.Advance(interval); 99 | calledTimes.Should().Be(2); 100 | } 101 | 102 | [Theory] 103 | [InlineData(1)] 104 | [InlineData(2)] 105 | [InlineData(3)] 106 | public void PeriodicTimer_WaitForNextTickAsync_completes_iterations(int expectedCallbacks) 107 | { 108 | var sut = new ManualTimeProvider(); 109 | var calledTimes = 0; 110 | var interval = TimeSpan.FromSeconds(1); 111 | var looper = WaitForNextTickInLoop(sut, () => calledTimes++, interval); 112 | 113 | sut.Advance(interval * expectedCallbacks); 114 | 115 | calledTimes.Should().Be(expectedCallbacks); 116 | } 117 | 118 | [Fact] 119 | public async void PeriodicTimer_WaitForNextTickAsync_exists_on_timer_Dispose() 120 | { 121 | var sut = new ManualTimeProvider(); 122 | var periodicTimer = sut.CreatePeriodicTimer(TimeSpan.FromSeconds(1)); 123 | var disposeTask = WaitForNextTickToReturnFalse(periodicTimer); 124 | sut.Advance(TimeSpan.FromSeconds(1)); 125 | 126 | periodicTimer.Dispose(); 127 | 128 | (await disposeTask).Should().BeFalse(); 129 | 130 | #if NET6_0_OR_GREATER && !NET8_0_OR_GREATER 131 | static async Task WaitForNextTickToReturnFalse(PeriodicTimerWrapper periodicTimer) 132 | #else 133 | static async Task WaitForNextTickToReturnFalse(System.Threading.PeriodicTimer periodicTimer) 134 | #endif 135 | { 136 | while (await periodicTimer.WaitForNextTickAsync(CancellationToken.None)) 137 | { 138 | } 139 | 140 | return false; 141 | } 142 | } 143 | 144 | [Fact] 145 | public void GetUtcNow_matches_time_when_WaitForNextTickAsync_is_invoked() 146 | { 147 | var sut = new ManualTimeProvider(); 148 | var startTime = sut.GetUtcNow(); 149 | var callbackTimes = new List(); 150 | var interval = TimeSpan.FromSeconds(5); 151 | var looper = WaitForNextTickInLoop(sut, () => callbackTimes.Add(sut.GetUtcNow()), interval); 152 | 153 | sut.Advance(interval * 3); 154 | 155 | callbackTimes.Should().Equal( 156 | startTime + interval * 1, 157 | startTime + interval * 2, 158 | startTime + interval * 3); 159 | } 160 | 161 | [Fact] 162 | public async void Cancelling_token_after_WaitForNextTickAsync_safe() 163 | { 164 | var sut = new ManualTimeProvider(); 165 | var interval = TimeSpan.FromSeconds(3); 166 | using var cts = new CancellationTokenSource(); 167 | var periodicTimer = sut.CreatePeriodicTimer(interval); 168 | var cleanCancelTask = CancelAfterWaitForNextTick(periodicTimer, cts); 169 | 170 | sut.Advance(interval); 171 | 172 | await cleanCancelTask; 173 | #if NET6_0_OR_GREATER && !NET8_0_OR_GREATER 174 | static async Task CancelAfterWaitForNextTick(PeriodicTimerWrapper periodicTimer, CancellationTokenSource cts) 175 | #else 176 | static async Task CancelAfterWaitForNextTick(System.Threading.PeriodicTimer periodicTimer, CancellationTokenSource cts) 177 | #endif 178 | { 179 | while (await periodicTimer.WaitForNextTickAsync(cts.Token)) 180 | { 181 | break; 182 | } 183 | cts.Cancel(); 184 | } 185 | } 186 | 187 | private static async Task WaitForNextTickInLoop(TimeProvider scheduler, Action callback, TimeSpan interval) 188 | { 189 | using var periodicTimer = scheduler.CreatePeriodicTimer(interval); 190 | while (await periodicTimer.WaitForNextTickAsync(CancellationToken.None).ConfigureAwait(false)) 191 | { 192 | callback(); 193 | } 194 | } 195 | } 196 | #endif -------------------------------------------------------------------------------- /test/TimeProviderExtensions.Tests/ManualTimeProviderTests.cs: -------------------------------------------------------------------------------- 1 | namespace TimeProviderExtensions; 2 | 3 | public class ManualTimeProviderTests 4 | { 5 | [Fact] 6 | public void Advance_updates_UtcNow() 7 | { 8 | var startTime = DateTimeOffset.UtcNow; 9 | var sut = new ManualTimeProvider(startTime); 10 | 11 | sut.Advance(TimeSpan.FromTicks(1)); 12 | 13 | sut.GetUtcNow().Should().Be(startTime + TimeSpan.FromTicks(1)); 14 | } 15 | 16 | [Fact] 17 | public void SetUtcNow_updates_UtcNow() 18 | { 19 | var startTime = DateTimeOffset.UtcNow; 20 | var sut = new ManualTimeProvider(startTime); 21 | 22 | sut.SetUtcNow(startTime + TimeSpan.FromTicks(1)); 23 | 24 | sut.GetUtcNow().Should().Be(startTime + TimeSpan.FromTicks(1)); 25 | } 26 | 27 | [Fact] 28 | public async Task Delay_callbacks_runs_synchronously() 29 | { 30 | // arrange 31 | var sut = new ManualTimeProvider(); 32 | var callbackCount = 0; 33 | var continuationTask = Continuation(sut, () => callbackCount++); 34 | 35 | // act 36 | sut.Advance(TimeSpan.FromSeconds(10)); 37 | 38 | // assert 39 | callbackCount.Should().Be(1); 40 | await continuationTask; 41 | 42 | static async Task Continuation(TimeProvider timeProvider, Action callback) 43 | { 44 | await timeProvider.Delay(TimeSpan.FromSeconds(10)); 45 | callback(); 46 | } 47 | } 48 | 49 | #if NET8_0_OR_GREATER 50 | [Fact] 51 | #else 52 | [Fact(Skip = "Bug in .NET 7 and earlier - https://github.com/dotnet/runtime/issues/92264")] 53 | #endif 54 | public async Task WaitAsync_callbacks_runs_synchronously() 55 | { 56 | // arrange 57 | var sut = new ManualTimeProvider(); 58 | var callbackCount = 0; 59 | var continuationTask = Continuation(sut, () => callbackCount++); 60 | 61 | // act 62 | sut.Advance(TimeSpan.FromSeconds(10)); 63 | 64 | // assert 65 | callbackCount.Should().Be(1); 66 | await continuationTask; 67 | 68 | static async Task Continuation(TimeProvider timeProvider, Action callback) 69 | { 70 | try 71 | { 72 | await Task 73 | .Delay(TimeSpan.FromDays(1)) 74 | .WaitAsync(TimeSpan.FromSeconds(10), timeProvider); 75 | } 76 | catch (TimeoutException) 77 | { 78 | callback(); 79 | } 80 | } 81 | } 82 | 83 | #if !NET8_0_OR_GREATER && NET6_0_OR_GREATER 84 | [Fact] 85 | public async Task Callbacks_happens_in_schedule_order() 86 | { 87 | var sut = new ManualTimeProvider(); 88 | var periodicTimer = sut.CreatePeriodicTimer(TimeSpan.FromSeconds(10)); 89 | var startTime = sut.GetUtcNow(); 90 | var callbacks = new List(); 91 | var callbacksTask = AsyncCallbacks(periodicTimer); 92 | 93 | sut.Advance(TimeSpan.FromSeconds(29)); 94 | 95 | callbacks.Should().HaveCount(4); 96 | callbacks.Should().ContainInOrder( 97 | startTime + TimeSpan.FromSeconds(10), 98 | startTime + TimeSpan.FromSeconds(13), 99 | startTime + TimeSpan.FromSeconds(20), 100 | startTime + TimeSpan.FromSeconds(23)); 101 | 102 | periodicTimer.Dispose(); 103 | await callbacksTask; 104 | 105 | async Task AsyncCallbacks(PeriodicTimerWrapper periodicTimer) 106 | { 107 | while (await periodicTimer.WaitForNextTickAsync().ConfigureAwait(false)) 108 | { 109 | callbacks.Add(sut.GetUtcNow()); 110 | await sut.Delay(TimeSpan.FromSeconds(3)); 111 | callbacks.Add(sut.GetUtcNow()); 112 | } 113 | } 114 | } 115 | #elif NET8_0_OR_GREATER 116 | [Fact] 117 | public async Task Callbacks_happens_in_schedule_order() 118 | { 119 | var sut = new ManualTimeProvider(); 120 | var periodicTimer = sut.CreatePeriodicTimer(TimeSpan.FromSeconds(10)); 121 | var startTime = sut.GetUtcNow(); 122 | var callbacks = new List(); 123 | var callbacksTask = AsyncCallbacks(periodicTimer); 124 | 125 | sut.Advance(TimeSpan.FromSeconds(29)); 126 | 127 | callbacks.Should().HaveCount(4); 128 | callbacks.Should().ContainInOrder( 129 | startTime + TimeSpan.FromSeconds(10), 130 | startTime + TimeSpan.FromSeconds(13), 131 | startTime + TimeSpan.FromSeconds(20), 132 | startTime + TimeSpan.FromSeconds(23)); 133 | 134 | periodicTimer.Dispose(); 135 | await callbacksTask; 136 | 137 | async Task AsyncCallbacks(PeriodicTimer timer) 138 | { 139 | while (await timer.WaitForNextTickAsync().ConfigureAwait(false)) 140 | { 141 | callbacks.Add(sut.GetUtcNow()); 142 | await sut.Delay(TimeSpan.FromSeconds(3)); 143 | callbacks.Add(sut.GetUtcNow()); 144 | } 145 | } 146 | } 147 | #endif 148 | 149 | [Fact] 150 | public void Timer_callback_GetUtcNow_AutoAdvance() 151 | { 152 | var oneSecond = TimeSpan.FromSeconds(1); 153 | var sut = new ManualTimeProvider() { AutoAdvanceBehavior = { UtcNowAdvanceAmount = oneSecond } }; 154 | 155 | using var t1 = sut.CreateTimer(_ => 156 | { 157 | sut.GetUtcNow(); 158 | }, null, TimeSpan.Zero, oneSecond); 159 | 160 | sut.GetUtcNow().Should().Be(sut.Start + oneSecond); 161 | } 162 | 163 | [Fact] 164 | public void GetUtcNow_with_ClockAdvanceAmount_gt_zero() 165 | { 166 | var sut = new ManualTimeProvider() { AutoAdvanceBehavior = { UtcNowAdvanceAmount = 1.Seconds() } }; 167 | 168 | var result = sut.GetUtcNow(); 169 | 170 | result.Should().Be(sut.Start); 171 | sut.GetUtcNow().Should().Be(sut.Start + 1.Seconds()); 172 | } 173 | 174 | [Fact] 175 | public void GetLocalNow_with_ClockAdvanceAmount_gt_zero() 176 | { 177 | var sut = new ManualTimeProvider() { AutoAdvanceBehavior = { UtcNowAdvanceAmount = 1.Seconds() } }; 178 | 179 | var result = sut.GetLocalNow(); 180 | 181 | result.Should().Be(sut.Start); 182 | sut.GetLocalNow().Should().Be(sut.Start + 1.Seconds()); 183 | } 184 | 185 | [Fact] 186 | public void GetTimestamp_with_TimestampAdvanceAmount_gt_zero() 187 | { 188 | var sut = new ManualTimeProvider() { AutoAdvanceBehavior = { TimestampAdvanceAmount = 1.Seconds() } }; 189 | 190 | var result = sut.GetTimestamp(); 191 | 192 | result.Should().Be(sut.Start.Ticks); 193 | sut.GetTimestamp().Should().Be(result + 1.Seconds().Ticks); 194 | } 195 | 196 | [Fact] 197 | public void GetElapsedTime_with_TimestampAdvanceAmount_gt_zero() 198 | { 199 | var sut = new ManualTimeProvider() { AutoAdvanceBehavior = { TimestampAdvanceAmount = 1.Seconds() } }; 200 | var start = sut.Start.Ticks; 201 | 202 | var result = sut.GetElapsedTime(start); 203 | 204 | result.Should().Be(0.Seconds()); 205 | sut.GetElapsedTime(start).Should().Be(1.Seconds()); 206 | } 207 | 208 | [Fact] 209 | public void Advance_zero() 210 | { 211 | var sut = new ManualTimeProvider(); 212 | 213 | sut.Advance(TimeSpan.Zero); 214 | 215 | sut.GetUtcNow().Should().Be(sut.Start); 216 | } 217 | 218 | [Fact] 219 | public void Jump_zero() 220 | { 221 | var sut = new ManualTimeProvider(); 222 | 223 | sut.Jump(TimeSpan.Zero); 224 | 225 | sut.GetUtcNow().Should().Be(sut.Start); 226 | } 227 | 228 | [Fact] 229 | public void Jump_throws_when_lt_zero() 230 | { 231 | var sut = new ManualTimeProvider(); 232 | 233 | var throws = () => sut.Jump(TimeSpan.FromTicks(-1)); 234 | 235 | throws.Should().Throw(); 236 | } 237 | 238 | [Fact] 239 | public void Jump_throws_going_back_in_time() 240 | { 241 | var sut = new ManualTimeProvider(); 242 | 243 | var throws = () => sut.Jump(sut.GetUtcNow() - TimeSpan.FromTicks(1)); 244 | 245 | throws.Should().Throw(); 246 | } 247 | 248 | [Fact] 249 | public async Task Multi_threaded_SetUtcNow() 250 | { 251 | var callbackCount = 0; 252 | var sut = new ManualTimeProvider(); 253 | using var timer = sut.CreateTimer(_ => 254 | { 255 | Thread.Sleep(20); 256 | callbackCount++; 257 | }, null, 1.Seconds(), 1.Seconds()); 258 | 259 | var tasks = Enumerable.Range(1, 100).Select(_ => Task.Run(() => sut.SetUtcNow(sut.Start + 1.Seconds()))); 260 | 261 | await Task.WhenAll(tasks).ConfigureAwait(false); 262 | callbackCount.Should().Be(1); 263 | } 264 | 265 | [Fact] 266 | public async Task Multi_threaded_Jump() 267 | { 268 | var callbackCount = 0; 269 | var sut = new ManualTimeProvider(); 270 | using var timer = sut.CreateTimer(_ => 271 | { 272 | Thread.Sleep(20); 273 | callbackCount++; 274 | }, null, 1.Seconds(), 1.Seconds()); 275 | 276 | var tasks = Enumerable.Range(1, 100).Select(_ => Task.Run(() => sut.Jump(sut.Start + 1.Seconds()))); 277 | 278 | await Task.WhenAll(tasks).ConfigureAwait(false); 279 | callbackCount.Should().Be(1); 280 | } 281 | 282 | [Fact] 283 | public void ActiveTimers_with_no_timers() 284 | { 285 | var sut = new ManualTimeProvider(); 286 | 287 | sut.ActiveTimers.Should().Be(0); 288 | } 289 | 290 | [Fact] 291 | public void ActiveTimers_with_active_timers() 292 | { 293 | var sut = new ManualTimeProvider(); 294 | 295 | using var timer = sut.CreateTimer(_ => { }, null, 1.Seconds(), Timeout.InfiniteTimeSpan); 296 | 297 | sut.ActiveTimers.Should().Be(1); 298 | } 299 | 300 | [Fact] 301 | public void ActiveTimers_with_inactive_timers() 302 | { 303 | var sut = new ManualTimeProvider(); 304 | 305 | using var timer = sut.CreateTimer(_ => { }, null, Timeout.InfiniteTimeSpan, Timeout.InfiniteTimeSpan); 306 | 307 | sut.ActiveTimers.Should().Be(0); 308 | } 309 | 310 | [Fact] 311 | public void ActiveTimers_with_after_timer_state_change() 312 | { 313 | var sut = new ManualTimeProvider(); 314 | 315 | using var timer = sut.CreateTimer(_ => { }, null, 1.Seconds(), Timeout.InfiniteTimeSpan); 316 | sut.Advance(1.Seconds()); 317 | 318 | sut.ActiveTimers.Should().Be(0); 319 | } 320 | 321 | [Fact] 322 | public void CreateManualTimer_with_custom_timer_type() 323 | { 324 | var sut = new CustomManualTimeProvider(); 325 | 326 | using var timer = sut.CreateTimer(_ => { }, null, Timeout.InfiniteTimeSpan, Timeout.InfiniteTimeSpan); 327 | 328 | timer.Should().BeOfType(); 329 | } 330 | 331 | [Theory] 332 | [InlineData(1)] 333 | [InlineData(10)] 334 | public void Active_timer_with_TimerAutoAdvanceTimes_gt_zero(int timerAutoTriggerCount) 335 | { 336 | var sut = new ManualTimeProvider { AutoAdvanceBehavior = { TimerAutoTriggerCount = timerAutoTriggerCount } }; 337 | var callbackTimes = 0; 338 | 339 | using var timer = sut.CreateTimer(_ => callbackTimes++, null, 1.Seconds(), 1.Seconds()); 340 | 341 | callbackTimes.Should().Be(timerAutoTriggerCount); 342 | sut.GetUtcNow().Should().Be(sut.Start + TimeSpan.FromSeconds(timerAutoTriggerCount)); 343 | sut.ActiveTimers.Should().Be(1); 344 | } 345 | 346 | [Theory] 347 | [InlineData(1)] 348 | [InlineData(10)] 349 | public void Inactive_timer_with_TimerAutoAdvanceTimes_gt_zero(int timerAutoTriggerCount) 350 | { 351 | var sut = new ManualTimeProvider { AutoAdvanceBehavior = { TimerAutoTriggerCount = timerAutoTriggerCount } }; 352 | var callbackTimes = 0; 353 | 354 | using var timer = sut.CreateTimer(_ => callbackTimes++, null, Timeout.InfiniteTimeSpan, 1.Seconds()); 355 | 356 | callbackTimes.Should().Be(0); 357 | sut.GetUtcNow().Should().Be(sut.Start); 358 | sut.ActiveTimers.Should().Be(0); 359 | } 360 | 361 | [Theory] 362 | [InlineData(1)] 363 | [InlineData(10)] 364 | public void Starting_timer_with_TimerAutoAdvanceTimes_gt_zero(int timerAutoTriggerCount) 365 | { 366 | var sut = new ManualTimeProvider { AutoAdvanceBehavior = { TimerAutoTriggerCount = timerAutoTriggerCount } }; 367 | var callbackTimes = 0; 368 | using var timer = sut.CreateTimer(_ => callbackTimes++, null, Timeout.InfiniteTimeSpan, Timeout.InfiniteTimeSpan); 369 | 370 | timer.Change(1.Seconds(), 1.Seconds()); 371 | 372 | callbackTimes.Should().Be(timerAutoTriggerCount); 373 | sut.GetUtcNow().Should().Be(sut.Start + TimeSpan.FromSeconds(timerAutoTriggerCount)); 374 | sut.ActiveTimers.Should().Be(1); 375 | } 376 | 377 | [Fact] 378 | public void Multiple_one_of_timers_with_TimerAutoAdvanceTimes_gt_zero() 379 | { 380 | var sut = new ManualTimeProvider { AutoAdvanceBehavior = { TimerAutoTriggerCount = 1 } }; 381 | var timer1CallbackTimes = 0; 382 | var timer2CallbackTimes = 0; 383 | 384 | using var timer1 = sut.CreateTimer(_ => timer1CallbackTimes++, null, 1.Seconds(), Timeout.InfiniteTimeSpan); 385 | using var timer2 = sut.CreateTimer(_ => timer2CallbackTimes++, null, 2.Seconds(), Timeout.InfiniteTimeSpan); 386 | 387 | sut.ActiveTimers.Should().Be(0); 388 | sut.GetUtcNow().Should().Be(sut.Start + 1.Seconds() + 2.Seconds()); 389 | timer1CallbackTimes.Should().Be(1); 390 | timer2CallbackTimes.Should().Be(1); 391 | } 392 | 393 | [Theory] 394 | [InlineData(1, 3, 1, 3)] 395 | [InlineData(10, 30, 10, 10 + 20)] 396 | public void Multiple_periodic_timers_with_TimerAutoAdvanceTimes_gt_zero(int timerAutoTriggerCount, int timer1ExpectedCallbackCount, int timer2ExpectedCallbackCount, int expectedSecondsSpend) 397 | { 398 | var sut = new ManualTimeProvider { AutoAdvanceBehavior = { TimerAutoTriggerCount = timerAutoTriggerCount } }; 399 | var timer1CallbackTimes = 0; 400 | var timer2CallbackTimes = 0; 401 | 402 | using var timer1 = sut.CreateTimer(_ => timer1CallbackTimes++, null, 1.Seconds(), 1.Seconds()); 403 | using var timer2 = sut.CreateTimer(_ => timer2CallbackTimes++, null, 2.Seconds(), 2.Seconds()); 404 | 405 | sut.ActiveTimers.Should().Be(2); 406 | sut.GetUtcNow().Should().Be(sut.Start + expectedSecondsSpend.Seconds()); 407 | timer1CallbackTimes.Should().Be(timer1ExpectedCallbackCount); 408 | timer2CallbackTimes.Should().Be(timer2ExpectedCallbackCount); 409 | } 410 | 411 | [Fact] 412 | public void Setting_AutoAdvanceBehavior_to_null() 413 | { 414 | var sut = new ManualTimeProvider(); 415 | 416 | sut.AutoAdvanceBehavior = null!; 417 | 418 | sut.AutoAdvanceBehavior.Should().BeEquivalentTo(new AutoAdvanceBehavior()); 419 | } 420 | 421 | private sealed class CustomManualTimeProvider : ManualTimeProvider 422 | { 423 | protected internal override ManualTimer CreateManualTimer(TimerCallback callback, object? state, ManualTimeProvider timeProvider) 424 | => new CustomManualTimer(callback, state, timeProvider); 425 | } 426 | 427 | private sealed class CustomManualTimer : ManualTimer 428 | { 429 | internal CustomManualTimer(TimerCallback callback, object? state, ManualTimeProvider timeProvider) 430 | : base(callback, state, timeProvider) 431 | { 432 | } 433 | } 434 | } -------------------------------------------------------------------------------- /test/TimeProviderExtensions.Tests/ManualTimeProviderTimestampTests.cs: -------------------------------------------------------------------------------- 1 | namespace TimeProviderExtensions; 2 | 3 | public class ManualTimeProviderTimestampTests 4 | { 5 | [Fact] 6 | public void TimestampFrequency_ten_mill() 7 | { 8 | var sut = new ManualTimeProvider(); 9 | 10 | sut.TimestampFrequency.Should().Be(10_000_000); 11 | } 12 | 13 | [Fact] 14 | public void GetTimestamp_increments_by_ticks() 15 | { 16 | var sut = new ManualTimeProvider(); 17 | var timestamp = sut.GetTimestamp(); 18 | 19 | sut.Advance(TimeSpan.FromTicks(1)); 20 | 21 | sut.GetTimestamp().Should().Be(timestamp + 1); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /test/TimeProviderExtensions.Tests/ManualTimeProviderWaitAsyncTests.cs: -------------------------------------------------------------------------------- 1 | #if NET6_0_OR_GREATER 2 | 3 | namespace TimeProviderExtensions; 4 | 5 | public class ManualTimeProviderWaitAsyncTests 6 | { 7 | private const uint MaxSupportedTimeout = 0xfffffffe; 8 | private static readonly TimeSpan DelayedTaskDelay = TimeSpan.FromMilliseconds(2); 9 | private static readonly string StringTaskResult = Guid.NewGuid().ToString(); 10 | 11 | private static async Task DelayedTask(TimeProvider provider) => await provider.Delay(DelayedTaskDelay); 12 | 13 | private static async Task DelayedStringTask(TimeProvider provider) 14 | { 15 | await provider.Delay(DelayedTaskDelay); 16 | return StringTaskResult; 17 | } 18 | 19 | public static TheoryData> TimeoutInvalidInvocations { get; } = 20 | new TheoryData> 21 | { 22 | sut => DelayedTask(sut).WaitAsync(TimeSpan.FromMilliseconds(-2), sut), 23 | sut => DelayedTask(sut).WaitAsync(TimeSpan.FromMilliseconds(-2), sut, CancellationToken.None), 24 | sut => DelayedStringTask(sut).WaitAsync(TimeSpan.FromMilliseconds(-2), sut), 25 | sut => DelayedStringTask(sut).WaitAsync(TimeSpan.FromMilliseconds(-2), sut, CancellationToken.None), 26 | sut => DelayedTask(sut).WaitAsync(TimeSpan.FromMilliseconds(MaxSupportedTimeout + 1), sut), 27 | sut => DelayedTask(sut).WaitAsync(TimeSpan.FromMilliseconds(MaxSupportedTimeout + 1), sut, CancellationToken.None), 28 | sut => DelayedStringTask(sut).WaitAsync(TimeSpan.FromMilliseconds(MaxSupportedTimeout + 1), sut), 29 | sut => DelayedStringTask(sut).WaitAsync(TimeSpan.FromMilliseconds(MaxSupportedTimeout + 1), sut, CancellationToken.None), 30 | }; 31 | 32 | [Theory, MemberData(nameof(TimeoutInvalidInvocations))] 33 | public async Task WaitAsync_timeout_input_validation(Func invalidInvocation) 34 | { 35 | var sut = new ManualTimeProvider(); 36 | 37 | await sut.Invoking(invalidInvocation) 38 | .Should() 39 | .ThrowAsync(); 40 | } 41 | 42 | public static TheoryData> CompletedInvocations { get; } = 43 | new TheoryData> 44 | { 45 | sut => Task.CompletedTask.WaitAsync(TimeSpan.FromMilliseconds(1), sut), 46 | sut => Task.CompletedTask.WaitAsync(TimeSpan.FromMilliseconds(1), sut, CancellationToken.None), 47 | sut => Task.CompletedTask.WaitAsync(TimeSpan.FromMilliseconds(1), sut), 48 | sut => Task.CompletedTask.WaitAsync(TimeSpan.FromMilliseconds(1), sut, CancellationToken.None), 49 | }; 50 | 51 | [Theory, MemberData(nameof(CompletedInvocations))] 52 | public void WaitAsync_completes_immediately_when_task_is_completed(Func completedInvocation) 53 | { 54 | var sut = new ManualTimeProvider(); 55 | 56 | var task = completedInvocation(sut); 57 | 58 | task.Should().CompletedSuccessfully(); 59 | } 60 | 61 | public static TheoryData> ImmediatelyCanceledInvocations { get; } = 62 | new TheoryData> 63 | { 64 | sut => DelayedTask(sut).WaitAsync(TimeSpan.FromMilliseconds(1), sut,new CancellationToken(canceled: true)), 65 | sut => DelayedStringTask(sut).WaitAsync(TimeSpan.FromMilliseconds(1), sut,new CancellationToken(canceled: true)), 66 | }; 67 | 68 | [Theory, MemberData(nameof(ImmediatelyCanceledInvocations))] 69 | public void WaitAsync_canceled_immediately_when_cancellationToken_is_set(Func canceledInvocation) 70 | { 71 | var sut = new ManualTimeProvider(); 72 | 73 | var task = canceledInvocation(sut); 74 | 75 | task.Should().Canceled(); 76 | } 77 | 78 | public static TheoryData> ImmediateTimedoutInvocations { get; } = 79 | new TheoryData> 80 | { 81 | sut => DelayedTask(sut).WaitAsync(TimeSpan.Zero, sut), 82 | sut => DelayedTask(sut).WaitAsync(TimeSpan.Zero, sut, CancellationToken.None), 83 | sut => DelayedStringTask(sut).WaitAsync(TimeSpan.Zero, sut), 84 | sut => DelayedStringTask(sut).WaitAsync(TimeSpan.Zero, sut, CancellationToken.None), 85 | }; 86 | 87 | [Theory, MemberData(nameof(ImmediateTimedoutInvocations))] 88 | public async Task WaitAsync_throws_immediately_when_timeout_is_zero(Func immediateTimedoutInvocation) 89 | { 90 | var sut = new ManualTimeProvider(); 91 | 92 | await sut.Invoking(immediateTimedoutInvocation) 93 | .Should() 94 | .ThrowExactlyAsync(); 95 | } 96 | 97 | public static TheoryData> ValidInvocations { get; } = 98 | new TheoryData> 99 | { 100 | sut => DelayedTask(sut).WaitAsync(TimeSpan.FromSeconds(1), sut), 101 | sut => DelayedTask(sut).WaitAsync(TimeSpan.FromSeconds(1), sut, CancellationToken.None), 102 | sut => DelayedStringTask(sut).WaitAsync(TimeSpan.FromSeconds(1), sut), 103 | sut => DelayedStringTask(sut).WaitAsync(TimeSpan.FromSeconds(1), sut, CancellationToken.None), 104 | }; 105 | 106 | [Theory, MemberData(nameof(ValidInvocations))] 107 | public async Task WaitAsync_completes_successfully(Func validInvocation) 108 | { 109 | var sut = new ManualTimeProvider(); 110 | var task = validInvocation(sut); 111 | 112 | sut.Advance(DelayedTaskDelay); 113 | 114 | await task 115 | .Should() 116 | .CompletedSuccessfullyAsync(); 117 | } 118 | 119 | public static TheoryData>> ValidStringInvocations { get; } = 120 | new TheoryData>> 121 | { 122 | sut => DelayedStringTask(sut).WaitAsync(TimeSpan.FromSeconds(1), sut), 123 | sut => DelayedStringTask(sut).WaitAsync(TimeSpan.FromSeconds(1), sut, CancellationToken.None), 124 | }; 125 | 126 | [Theory, MemberData(nameof(ValidStringInvocations))] 127 | public async Task WaitAsync_of_T_completes_successfully(Func> validInvocation) 128 | { 129 | var sut = new ManualTimeProvider(); 130 | var task = validInvocation(sut); 131 | 132 | sut.Advance(DelayedTaskDelay); 133 | 134 | await task 135 | .Should() 136 | .CompletedSuccessfullyAsync(); 137 | task.Result.Should().Be(StringTaskResult); 138 | } 139 | 140 | public static TheoryData> TimedoutInvocations { get; } = 141 | new TheoryData> 142 | { 143 | (sut, timeout) => DelayedTask(sut).WaitAsync(timeout, sut), 144 | (sut, timeout) => DelayedTask(sut).WaitAsync(timeout, sut, CancellationToken.None), 145 | (sut, timeout) => DelayedStringTask(sut).WaitAsync(timeout, sut), 146 | (sut, timeout) => DelayedStringTask(sut).WaitAsync(timeout, sut, CancellationToken.None), 147 | }; 148 | 149 | [Theory, MemberData(nameof(TimedoutInvocations))] 150 | public async Task WaitAsync_throws_when_timeout_is_reached(Func invocationWithTime) 151 | { 152 | var sut = new ManualTimeProvider(); 153 | var task = invocationWithTime(sut, TimeSpan.FromMilliseconds(1)); 154 | 155 | sut.Advance(TimeSpan.FromMilliseconds(1)); 156 | 157 | await task.Awaiting(x => x) 158 | .Should() 159 | .ThrowExactlyAsync(); 160 | } 161 | 162 | public static TheoryData> CancelledInvocations { get; } = 163 | new TheoryData> 164 | { 165 | (sut, token) => DelayedTask(sut).WaitAsync(TimeSpan.FromMilliseconds(1), sut, token), 166 | (sut, token) => DelayedStringTask(sut).WaitAsync(TimeSpan.FromMilliseconds(1), sut, token), 167 | }; 168 | 169 | [Theory, MemberData(nameof(CancelledInvocations))] 170 | public async Task WaitAsync_throws_when_token_is_canceled(Func invocationWithCancelToken) 171 | { 172 | using var cts = new CancellationTokenSource(); 173 | var sut = new ManualTimeProvider(); 174 | var task = invocationWithCancelToken(sut, cts.Token); 175 | 176 | cts.Cancel(); 177 | 178 | await task.Awaiting(x => x) 179 | .Should() 180 | .ThrowExactlyAsync(); 181 | } 182 | 183 | [Fact] 184 | public void WaitAsync_with_TimerAutoInvokeCount_gt_zero() 185 | { 186 | var tcs = new TaskCompletionSource(); 187 | var sut = new ManualTimeProvider { AutoAdvanceBehavior = { TimerAutoTriggerCount= 1 } }; 188 | 189 | var task = tcs.Task.WaitAsync(1.Seconds(), sut); 190 | 191 | task.Status.Should().Be(TaskStatus.Faulted); 192 | task.Exception!.InnerException.Should().BeOfType(); 193 | sut.ActiveTimers.Should().Be(0); 194 | } 195 | 196 | [Fact] 197 | public async Task Active_timer_with_TimerAutoAdvanceTimes_one_other_thread() 198 | { 199 | var sut = new ManualTimeProvider { AutoAdvanceBehavior = { TimerAutoTriggerCount = 1 } }; 200 | var tcs = new TaskCompletionSource(); 201 | using var t1 = sut.CreateTimer(_ => { }, null, 1.Seconds(), 1.Seconds()); 202 | using var t2 = sut.CreateTimer(_ => { }, null, 1.Minutes(), 1.Minutes()); 203 | 204 | var task = Task.Run(async () => await tcs.Task.WaitAsync(10.Seconds(), sut)); 205 | 206 | try 207 | { 208 | await task; 209 | } 210 | catch 211 | { 212 | task.Status.Should().Be(TaskStatus.Faulted); 213 | task.Exception!.InnerException.Should().BeOfType(); 214 | } 215 | 216 | sut.ActiveTimers.Should().Be(2); 217 | } 218 | } 219 | #endif -------------------------------------------------------------------------------- /test/TimeProviderExtensions.Tests/ManualTimerTests.cs: -------------------------------------------------------------------------------- 1 | namespace TimeProviderExtensions; 2 | 3 | public class ManualTimerTests 4 | { 5 | [Fact] 6 | public void ToString_with_disposed_timer() 7 | { 8 | var timeProvider = new ManualTimeProvider(); 9 | 10 | var sut = timeProvider.CreateTimer(_ => { }, null, Timeout.InfiniteTimeSpan, Timeout.InfiniteTimeSpan); 11 | sut.Dispose(); 12 | 13 | sut.ToString().Should().Be("Timer is disposed."); 14 | } 15 | 16 | [Fact] 17 | public void ToString_with_disabled_timer() 18 | { 19 | var timeProvider = new ManualTimeProvider(); 20 | 21 | var sut = timeProvider.CreateTimer(_ => { }, null, Timeout.InfiniteTimeSpan, Timeout.InfiniteTimeSpan); 22 | 23 | sut.ToString().Should().Be("Timer is disabled. DueTime: Infinite. Period: Infinite."); 24 | } 25 | 26 | [Fact] 27 | public void ToString_with_immidiate_invokcation_timer() 28 | { 29 | var timeProvider = new ManualTimeProvider(); 30 | 31 | var sut = timeProvider.CreateTimer(_ => { }, null, TimeSpan.Zero, Timeout.InfiniteTimeSpan); 32 | 33 | sut.ToString().Should().Be("Timer is disabled. DueTime: 00:00:00. Period: Infinite."); 34 | } 35 | 36 | [Fact] 37 | public void ToString_with_periodic_timer() 38 | { 39 | var timeProvider = new ManualTimeProvider(); 40 | 41 | var sut = timeProvider.CreateTimer(_ => { }, null, TimeSpan.Zero, new TimeSpan(hours: 23, minutes: 33, seconds: 2)); 42 | 43 | sut.ToString().Should().Be("Next callback: 2000-01-01T23:33:02.000. DueTime: 00:00:00. Period: 23:33:02."); 44 | } 45 | 46 | [Fact] 47 | public void ToString_with_duetime_periodic_timer() 48 | { 49 | var timeProvider = new ManualTimeProvider(); 50 | 51 | var sut = timeProvider.CreateTimer(_ => { }, null, 2.Seconds(), new TimeSpan(hours: 23, minutes: 33, seconds: 2)); 52 | 53 | sut.ToString().Should().Be("Next callback: 2000-01-01T00:00:02.000. DueTime: 00:00:02. Period: 23:33:02."); 54 | } 55 | 56 | [Fact] 57 | public void CallbackTime_with_inactive_timer() 58 | { 59 | var timeProvider = new ManualTimeProvider(); 60 | 61 | var sut = (ManualTimer)timeProvider.CreateTimer(_ => { }, null, Timeout.InfiniteTimeSpan, Timeout.InfiniteTimeSpan); 62 | 63 | sut.CallbackTime.Should().Be(default(DateTimeOffset?)); 64 | } 65 | 66 | [Fact] 67 | public void CallbackTime_with_active_timer() 68 | { 69 | var timeProvider = new ManualTimeProvider(); 70 | 71 | var sut = (ManualTimer)timeProvider.CreateTimer(_ => { }, null, 2.Seconds(), Timeout.InfiniteTimeSpan); 72 | 73 | sut.CallbackTime.Should().Be(timeProvider.Start + 2.Seconds()); 74 | } 75 | 76 | [Fact] 77 | public void IsActive_with_disposed_timer() 78 | { 79 | var timeProvider = new ManualTimeProvider(); 80 | 81 | var sut = (ManualTimer)timeProvider.CreateTimer(_ => { }, null, Timeout.InfiniteTimeSpan, Timeout.InfiniteTimeSpan); 82 | sut.Dispose(); 83 | 84 | sut.IsActive.Should().BeFalse(); 85 | } 86 | 87 | [Fact] 88 | public void IsActive_with_inactive_timer() 89 | { 90 | var timeProvider = new ManualTimeProvider(); 91 | 92 | var sut = (ManualTimer)timeProvider.CreateTimer(_ => { }, null, Timeout.InfiniteTimeSpan, Timeout.InfiniteTimeSpan); 93 | 94 | sut.IsActive.Should().BeFalse(); 95 | } 96 | 97 | [Fact] 98 | public void IsActive_with_active_timer() 99 | { 100 | var timeProvider = new ManualTimeProvider(); 101 | 102 | var sut = (ManualTimer)timeProvider.CreateTimer(_ => { }, null, 2.Seconds(), Timeout.InfiniteTimeSpan); 103 | 104 | sut.IsActive.Should().BeTrue(); 105 | } 106 | 107 | [Fact] 108 | public void CallbackInvokeCount_with_inactive_timer() 109 | { 110 | var timeProvider = new ManualTimeProvider(); 111 | 112 | var sut = (ManualTimer)timeProvider.CreateTimer(_ => { }, null, Timeout.InfiniteTimeSpan, Timeout.InfiniteTimeSpan); 113 | 114 | sut.CallbackInvokeCount.Should().Be(0); 115 | } 116 | 117 | [Fact] 118 | public void CallbackInvokeCount_with_active_timer() 119 | { 120 | var timeProvider = new ManualTimeProvider() { AutoAdvanceBehavior = { TimerAutoTriggerCount = 1 } }; 121 | 122 | var sut = (ManualTimer)timeProvider.CreateTimer(_ => { }, null, 2.Seconds(), Timeout.InfiniteTimeSpan); 123 | 124 | sut.CallbackInvokeCount.Should().Be(1); 125 | } 126 | 127 | 128 | [Fact] 129 | public void CallbackInvokeCount_with_disposed_timer() 130 | { 131 | var timeProvider = new ManualTimeProvider() { AutoAdvanceBehavior = { TimerAutoTriggerCount = 1 } }; 132 | 133 | var sut = (ManualTimer)timeProvider.CreateTimer(_ => { }, null, 2.Seconds(), Timeout.InfiniteTimeSpan); 134 | sut.Dispose(); 135 | 136 | sut.CallbackInvokeCount.Should().Be(1); 137 | } 138 | 139 | [Fact] 140 | public void CreateTimer_with_positive_DueTime_and_infinite_Period() 141 | { 142 | var callbackCount = 0; 143 | var dueTime = TimeSpan.FromSeconds(1); 144 | var period = Timeout.InfiniteTimeSpan; 145 | var sut = new ManualTimeProvider(); 146 | using var timer = sut.CreateTimer(_ => callbackCount++, null, dueTime, period); 147 | 148 | sut.Advance(dueTime); 149 | callbackCount.Should().Be(1); 150 | 151 | sut.Advance(dueTime); 152 | callbackCount.Should().Be(1); 153 | } 154 | 155 | [Fact] 156 | public void CreateTimer_with_positive_DueTime_and_Period() 157 | { 158 | var callbackCount = 0; 159 | var dueTime = TimeSpan.FromSeconds(1); 160 | var period = TimeSpan.FromSeconds(2); 161 | var sut = new ManualTimeProvider(); 162 | using var timer = sut.CreateTimer(_ => callbackCount++, null, dueTime, period); 163 | 164 | sut.Advance(dueTime); 165 | callbackCount.Should().Be(1); 166 | 167 | sut.Advance(period); 168 | callbackCount.Should().Be(2); 169 | 170 | sut.Advance(period); 171 | callbackCount.Should().Be(3); 172 | } 173 | 174 | [Fact] 175 | public void CreateTimer_with_infinite_DueTime_and_Period() 176 | { 177 | var callbackCount = 0; 178 | var dueTime = Timeout.InfiniteTimeSpan; 179 | var period = Timeout.InfiniteTimeSpan; 180 | var sut = new ManualTimeProvider(); 181 | using var timer = sut.CreateTimer(_ => callbackCount++, null, dueTime, period); 182 | 183 | sut.Advance(TimeSpan.FromSeconds(1)); 184 | 185 | callbackCount.Should().Be(0); 186 | } 187 | 188 | [Fact] 189 | public void Change_timer_from_stopped_to_started() 190 | { 191 | // Arrange 192 | var callbackCount = 0; 193 | var sut = new ManualTimeProvider(); 194 | using var timer = sut.CreateTimer(_ => callbackCount++, null, Timeout.InfiniteTimeSpan, Timeout.InfiniteTimeSpan); 195 | var dueTime = TimeSpan.FromSeconds(1); 196 | var period = TimeSpan.FromSeconds(2); 197 | 198 | // Act 199 | timer.Change(dueTime, period); 200 | 201 | // Assert 202 | sut.Advance(dueTime); 203 | callbackCount.Should().Be(1); 204 | 205 | sut.Advance(period); 206 | callbackCount.Should().Be(2); 207 | 208 | sut.Advance(period); 209 | callbackCount.Should().Be(3); 210 | } 211 | 212 | [Fact] 213 | public void Change_timer() 214 | { 215 | // Arrange 216 | var callbackCount = 0; 217 | var sut = new ManualTimeProvider(); 218 | var originalDueTime = TimeSpan.FromSeconds(3); 219 | var period = TimeSpan.FromSeconds(5); 220 | using var timer = sut.CreateTimer(_ => callbackCount++, null, originalDueTime, period); 221 | var dueTime = TimeSpan.FromSeconds(4); 222 | 223 | // Change to a larger value 224 | timer.Change(dueTime, period); 225 | 226 | // Assert that previous dueTime is ignored 227 | sut.Advance(originalDueTime); 228 | callbackCount.Should().Be(0); 229 | 230 | sut.Advance(dueTime); 231 | callbackCount.Should().Be(1); 232 | 233 | sut.Advance(period); 234 | callbackCount.Should().Be(2); 235 | } 236 | 237 | [Fact] 238 | public void Timer_callback_invoked_multiple_times_single_advance() 239 | { 240 | var sut = new ManualTimeProvider(); 241 | var callbackCount = 0; 242 | var dueTime = TimeSpan.FromSeconds(3); 243 | var period = TimeSpan.FromSeconds(5); 244 | using var timer = sut.CreateTimer(_ => callbackCount++, null, dueTime, period); 245 | 246 | sut.Advance(TimeSpan.FromSeconds(13)); 247 | 248 | callbackCount.Should().Be(3); 249 | } 250 | 251 | [Fact] 252 | public void Advancing_GetUtcNow_matches_time_at_callback_time() 253 | { 254 | var sut = new ManualTimeProvider(); 255 | var startTime = sut.GetUtcNow(); 256 | var callbackTimes = new List(); 257 | var interval = TimeSpan.FromSeconds(3); 258 | using var timer = sut.CreateTimer(_ => callbackTimes.Add(sut.GetUtcNow()), null, interval, interval); 259 | 260 | sut.Advance(interval + interval + interval); 261 | 262 | callbackTimes.Should().ContainInOrder( 263 | startTime + interval, 264 | startTime + interval + interval, 265 | startTime + interval + interval + interval); 266 | } 267 | 268 | [Fact] 269 | public void Disposing_timer_in_callback() 270 | { 271 | var interval = TimeSpan.FromSeconds(3); 272 | var sut = new ManualTimeProvider(); 273 | ITimer timer = default!; 274 | timer = sut.CreateTimer(_ => timer.Dispose(), null, interval, interval); 275 | 276 | sut.Advance(interval); 277 | } 278 | 279 | [Fact] 280 | public void Advancing_causes_multiple_timers_invokes_callback_in_order() 281 | { 282 | var callbacks = new List<(int TimerNumber, TimeSpan CallbackTime)>(); 283 | var sut = new ManualTimeProvider(); 284 | var startTime = sut.GetTimestamp(); 285 | using var timer1 = sut.CreateTimer(_ => callbacks.Add((1, sut.GetElapsedTime(startTime))), null, TimeSpan.FromSeconds(2), TimeSpan.FromSeconds(2)); 286 | using var timer2 = sut.CreateTimer(_ => callbacks.Add((2, sut.GetElapsedTime(startTime))), null, TimeSpan.FromSeconds(3), TimeSpan.FromSeconds(3)); 287 | 288 | sut.Advance(TimeSpan.FromSeconds(11)); 289 | 290 | callbacks.Should().Equal( 291 | (1, TimeSpan.FromSeconds(2)), 292 | (2, TimeSpan.FromSeconds(3)), 293 | (1, TimeSpan.FromSeconds(4)), 294 | (2, TimeSpan.FromSeconds(6)), 295 | (1, TimeSpan.FromSeconds(6)), 296 | (1, TimeSpan.FromSeconds(8)), 297 | (2, TimeSpan.FromSeconds(9)), 298 | (1, TimeSpan.FromSeconds(10))); 299 | } 300 | 301 | [Fact] 302 | public void Jumping_GetUtcNow_matches_jump_target() 303 | { 304 | var sut = new ManualTimeProvider(); 305 | var startTime = sut.GetUtcNow(); 306 | var callbackTimes = new List(); 307 | var interval = TimeSpan.FromSeconds(3); 308 | var target = interval + interval + interval; 309 | using var timer = sut.CreateTimer(_ => callbackTimes.Add(sut.GetUtcNow()), null, interval, interval); 310 | 311 | sut.Jump(target); 312 | 313 | callbackTimes.Should().Equal(startTime + target, startTime + target, startTime + target); 314 | } 315 | 316 | [Fact] 317 | public void Jumping_past_longer_than_recurrence() 318 | { 319 | var sut = new ManualTimeProvider(); 320 | var startTime = sut.GetUtcNow(); 321 | var callbackTimes = new List(); 322 | var interval = TimeSpan.FromSeconds(3); 323 | var target = TimeSpan.FromSeconds(4); 324 | using var timer = sut.CreateTimer(_ => callbackTimes.Add(sut.GetUtcNow()), null, interval, interval); 325 | 326 | sut.Jump(target); 327 | 328 | callbackTimes.Should().Equal(startTime + target); 329 | } 330 | 331 | [Fact] 332 | public void Jumping_causes_multiple_timers_invokes_callback_in_order() 333 | { 334 | var sut = new ManualTimeProvider(); 335 | var callbacks = new List<(int timerId, TimeSpan callbackTime)>(); 336 | var startTime = sut.GetTimestamp(); 337 | using var timer1 = sut.CreateTimer(_ => callbacks.Add((1, sut.GetElapsedTime(startTime))), null, TimeSpan.FromMilliseconds(3), TimeSpan.FromMilliseconds(3)); 338 | using var timer2 = sut.CreateTimer(_ => callbacks.Add((2, sut.GetElapsedTime(startTime))), null, TimeSpan.FromMilliseconds(3), TimeSpan.FromMilliseconds(3)); 339 | using var timer3 = sut.CreateTimer(_ => callbacks.Add((3, sut.GetElapsedTime(startTime))), null, TimeSpan.FromMilliseconds(6), TimeSpan.FromMilliseconds(5)); 340 | 341 | sut.Jump(TimeSpan.FromMilliseconds(3)); 342 | sut.Jump(TimeSpan.FromMilliseconds(3)); 343 | sut.Jump(TimeSpan.FromMilliseconds(3)); 344 | sut.Jump(TimeSpan.FromMilliseconds(2)); 345 | 346 | callbacks.Should().Equal( 347 | (1, TimeSpan.FromMilliseconds(3)), 348 | (2, TimeSpan.FromMilliseconds(3)), 349 | (3, TimeSpan.FromMilliseconds(6)), 350 | (1, TimeSpan.FromMilliseconds(6)), 351 | (2, TimeSpan.FromMilliseconds(6)), 352 | (1, TimeSpan.FromMilliseconds(9)), 353 | (2, TimeSpan.FromMilliseconds(9)), 354 | (3, TimeSpan.FromMilliseconds(11))); 355 | } 356 | } 357 | -------------------------------------------------------------------------------- /test/TimeProviderExtensions.Tests/Microsoft.Extensions.Time.Testing.Test/FakeTimeProviderTests.cs: -------------------------------------------------------------------------------- 1 | // This file originally copied from the following URL, and only modified 2 | // to allow the original tests to run against the ManualTimeProvider. 3 | // https://github.com/dotnet/extensions/blob/285e8a05274da583a557f66715f7773dc67e3799/test/Libraries/Microsoft.Extensions.TimeProvider.Testing.Tests/FakeTimeProviderTests.cs 4 | 5 | // Licensed to the .NET Foundation under one or more agreements. 6 | // The .NET Foundation licenses this file to you under the MIT license. 7 | 8 | namespace Microsoft.Extensions.Time.Testing.Test; 9 | 10 | using TimeProviderExtensions; 11 | using FakeTimeProvider = TimeProviderExtensions.ManualTimeProvider; 12 | 13 | public class ManualTimeProviderTests 14 | { 15 | [Fact] 16 | public void DefaultCtor() 17 | { 18 | var timeProvider = new FakeTimeProvider(); 19 | 20 | var now = timeProvider.GetUtcNow(); 21 | var timestamp = timeProvider.GetTimestamp(); 22 | var frequency = timeProvider.TimestampFrequency; 23 | 24 | Assert.Equal(2000, now.Year); 25 | Assert.Equal(1, now.Month); 26 | Assert.Equal(1, now.Day); 27 | Assert.Equal(0, now.Hour); 28 | Assert.Equal(0, now.Minute); 29 | Assert.Equal(0, now.Second); 30 | Assert.Equal(0, now.Millisecond); 31 | Assert.Equal(TimeSpan.Zero, now.Offset); 32 | Assert.Equal(10_000_000, frequency); 33 | Assert.Equal(new AutoAdvanceBehavior(), timeProvider.AutoAdvanceBehavior); 34 | 35 | var timestamp2 = timeProvider.GetTimestamp(); 36 | var frequency2 = timeProvider.TimestampFrequency; 37 | var now2 = timeProvider.GetUtcNow(); 38 | 39 | Assert.Equal(now, now2); 40 | Assert.Equal(frequency, frequency2); 41 | Assert.Equal(timestamp, timestamp2); 42 | } 43 | 44 | [Fact] 45 | public void RichCtor() 46 | { 47 | var timeProvider = new FakeTimeProvider(new DateTimeOffset(2001, 2, 3, 4, 5, 6, TimeSpan.Zero)); 48 | 49 | timeProvider.Advance(TimeSpan.FromMilliseconds(8)); 50 | var pnow = timeProvider.GetTimestamp(); 51 | var frequency = timeProvider.TimestampFrequency; 52 | var now = timeProvider.GetUtcNow(); 53 | 54 | Assert.Equal(2001, now.Year); 55 | Assert.Equal(2, now.Month); 56 | Assert.Equal(3, now.Day); 57 | Assert.Equal(4, now.Hour); 58 | Assert.Equal(5, now.Minute); 59 | Assert.Equal(6, now.Second); 60 | Assert.Equal(TimeSpan.Zero, now.Offset); 61 | Assert.Equal(8, now.Millisecond); 62 | Assert.Equal(10_000_000, frequency); 63 | Assert.Equal(new AutoAdvanceBehavior(), timeProvider.AutoAdvanceBehavior); 64 | 65 | timeProvider.Advance(TimeSpan.FromMilliseconds(8)); 66 | var pnow2 = timeProvider.GetTimestamp(); 67 | var frequency2 = timeProvider.TimestampFrequency; 68 | now = timeProvider.GetUtcNow(); 69 | 70 | Assert.Equal(2001, now.Year); 71 | Assert.Equal(2, now.Month); 72 | Assert.Equal(3, now.Day); 73 | Assert.Equal(4, now.Hour); 74 | Assert.Equal(5, now.Minute); 75 | Assert.Equal(6, now.Second); 76 | Assert.Equal(16, now.Millisecond); 77 | Assert.Equal(frequency, frequency2); 78 | Assert.True(pnow2 > pnow); 79 | } 80 | 81 | [Fact] 82 | public void LocalTimeZoneIsUtc() 83 | { 84 | var timeProvider = new FakeTimeProvider(); 85 | var localTimeZone = timeProvider.LocalTimeZone; 86 | 87 | Assert.Equal(TimeZoneInfo.Utc, localTimeZone); 88 | } 89 | 90 | [Fact] 91 | public void SetLocalTimeZoneWorks() 92 | { 93 | var timeProvider = new FakeTimeProvider(); 94 | 95 | var localTimeZone = timeProvider.LocalTimeZone; 96 | Assert.Equal(TimeZoneInfo.Utc, localTimeZone); 97 | 98 | var tz = TimeZoneInfo.CreateCustomTimeZone("DUMMY", TimeSpan.FromHours(2), null, null); 99 | timeProvider.SetLocalTimeZone(tz); 100 | Assert.Equal(timeProvider.LocalTimeZone, tz); 101 | } 102 | 103 | [Fact] 104 | public void GetTimestampSyncWithUtcNow() 105 | { 106 | var timeProvider = new FakeTimeProvider(new DateTimeOffset(2001, 2, 3, 4, 5, 6, TimeSpan.Zero)); 107 | 108 | var initialTimeUtcNow = timeProvider.GetUtcNow(); 109 | var initialTimestamp = timeProvider.GetTimestamp(); 110 | 111 | timeProvider.SetUtcNow(timeProvider.GetUtcNow().AddMilliseconds(1234)); 112 | 113 | var finalTimeUtcNow = timeProvider.GetUtcNow(); 114 | var finalTimeTimestamp = timeProvider.GetTimestamp(); 115 | 116 | var utcDelta = finalTimeUtcNow - initialTimeUtcNow; 117 | var perfDelta = finalTimeTimestamp - initialTimestamp; 118 | var elapsedTime = timeProvider.GetElapsedTime(initialTimestamp, finalTimeTimestamp); 119 | 120 | Assert.Equal(1, utcDelta.Seconds); 121 | Assert.Equal(234, utcDelta.Milliseconds); 122 | Assert.Equal(1234D, utcDelta.TotalMilliseconds); 123 | Assert.Equal(1.234D, (double)perfDelta / timeProvider.TimestampFrequency, 3); 124 | Assert.Equal(1234, elapsedTime.TotalMilliseconds); 125 | } 126 | 127 | [Fact] 128 | public void AdvanceGoesForward() 129 | { 130 | var timeProvider = new FakeTimeProvider(new DateTimeOffset(2001, 2, 3, 4, 5, 6, TimeSpan.Zero)); 131 | 132 | var initialTimeUtcNow = timeProvider.GetUtcNow(); 133 | var initialTimestamp = timeProvider.GetTimestamp(); 134 | 135 | timeProvider.Advance(TimeSpan.FromMilliseconds(1234)); 136 | 137 | var finalTimeUtcNow = timeProvider.GetUtcNow(); 138 | var finalTimeTimestamp = timeProvider.GetTimestamp(); 139 | 140 | var utcDelta = finalTimeUtcNow - initialTimeUtcNow; 141 | var perfDelta = finalTimeTimestamp - initialTimestamp; 142 | var elapsedTime = timeProvider.GetElapsedTime(initialTimestamp, finalTimeTimestamp); 143 | 144 | Assert.Equal(1, utcDelta.Seconds); 145 | Assert.Equal(234, utcDelta.Milliseconds); 146 | Assert.Equal(1234D, utcDelta.TotalMilliseconds); 147 | Assert.Equal(1.234D, (double)perfDelta / timeProvider.TimestampFrequency, 3); 148 | Assert.Equal(1234, elapsedTime.TotalMilliseconds); 149 | } 150 | 151 | [Fact] 152 | public void TimeCannotGoBackwards() 153 | { 154 | var timeProvider = new FakeTimeProvider(); 155 | 156 | Assert.Throws(() => timeProvider.Advance(TimeSpan.FromTicks(-1))); 157 | Assert.Throws(() => timeProvider.SetUtcNow(timeProvider.GetUtcNow() - TimeSpan.FromTicks(1))); 158 | } 159 | 160 | [Fact] 161 | public void ToStr() 162 | { 163 | var dto = new DateTimeOffset(new DateTime(2022, 1, 2, 3, 4, 5, 6), TimeSpan.Zero); 164 | 165 | var timeProvider = new FakeTimeProvider(dto); 166 | Assert.Equal("2022-01-02T03:04:05.006", timeProvider.ToString()); 167 | } 168 | 169 | private readonly TimeSpan _infiniteTimeout = TimeSpan.FromMilliseconds(-1); 170 | 171 | [Fact] 172 | public void Delay_InvalidArgs() 173 | { 174 | var timeProvider = new FakeTimeProvider(); 175 | _ = Assert.ThrowsAsync(() => timeProvider.Delay(TimeSpan.FromTicks(-1), CancellationToken.None)); 176 | _ = Assert.ThrowsAsync(() => timeProvider.Delay(_infiniteTimeout, CancellationToken.None)); 177 | } 178 | 179 | [Fact] 180 | public async Task Delay_Zero() 181 | { 182 | var timeProvider = new FakeTimeProvider(); 183 | var t = timeProvider.Delay(TimeSpan.Zero, CancellationToken.None); 184 | await t; 185 | 186 | Assert.True(t.IsCompleted && !t.IsFaulted); 187 | } 188 | 189 | [Fact] 190 | public async Task Delay_Timeout() 191 | { 192 | var timeProvider = new FakeTimeProvider(); 193 | 194 | var delay = timeProvider.Delay(TimeSpan.FromMilliseconds(1), CancellationToken.None); 195 | timeProvider.Advance(TimeSpan.FromMilliseconds(1)); 196 | await delay; 197 | 198 | Assert.True(delay.IsCompleted); 199 | Assert.False(delay.IsFaulted); 200 | Assert.False(delay.IsCanceled); 201 | } 202 | 203 | [Fact] 204 | public async Task Delay_Cancelled() 205 | { 206 | var timeProvider = new FakeTimeProvider(); 207 | 208 | using var cs = new CancellationTokenSource(); 209 | var delay = timeProvider.Delay(_infiniteTimeout, cs.Token); 210 | Assert.False(delay.IsCompleted); 211 | 212 | cs.Cancel(); 213 | 214 | await Assert.ThrowsAsync(async () => await delay); 215 | } 216 | 217 | [Fact] 218 | public async Task CreateSource() 219 | { 220 | var timeProvider = new FakeTimeProvider(); 221 | 222 | using var cts = timeProvider.CreateCancellationTokenSource(TimeSpan.FromMilliseconds(1)); 223 | timeProvider.Advance(TimeSpan.FromMilliseconds(1)); 224 | 225 | await Assert.ThrowsAsync(() => timeProvider.Delay(TimeSpan.FromTicks(1), cts.Token)); 226 | } 227 | 228 | [Fact] 229 | public async Task WaitAsync() 230 | { 231 | var timeProvider = new FakeTimeProvider(); 232 | var source = new TaskCompletionSource(); 233 | 234 | #if NET8_0_OR_GREATER 235 | await Assert.ThrowsAsync(() => source.Task.WaitAsync(TimeSpan.FromTicks(-1), timeProvider, CancellationToken.None)); 236 | #else 237 | await Assert.ThrowsAsync(() => source.Task.WaitAsync(TimeSpan.FromTicks(-1), timeProvider, CancellationToken.None)); 238 | #endif 239 | await Assert.ThrowsAsync(() => source.Task.WaitAsync(TimeSpan.FromMilliseconds(-2), timeProvider, CancellationToken.None)); 240 | 241 | var t = source.Task.WaitAsync(TimeSpan.FromSeconds(100000), timeProvider, CancellationToken.None); 242 | while (!t.IsCompleted) 243 | { 244 | timeProvider.Advance(TimeSpan.FromMilliseconds(1)); 245 | await Task.Delay(1); 246 | _ = source.TrySetResult(true); 247 | } 248 | 249 | Assert.True(t.IsCompleted); 250 | Assert.False(t.IsFaulted); 251 | Assert.False(t.IsCanceled); 252 | } 253 | 254 | [Fact] 255 | public async Task WaitAsync_InfiniteTimeout() 256 | { 257 | var timeProvider = new FakeTimeProvider(); 258 | var source = new TaskCompletionSource(); 259 | 260 | var t = source.Task.WaitAsync(_infiniteTimeout, timeProvider, CancellationToken.None); 261 | while (!t.IsCompleted) 262 | { 263 | timeProvider.Advance(TimeSpan.FromMilliseconds(1)); 264 | await Task.Delay(1); 265 | _ = source.TrySetResult(true); 266 | } 267 | 268 | Assert.True(t.IsCompleted); 269 | Assert.False(t.IsFaulted); 270 | Assert.False(t.IsCanceled); 271 | } 272 | 273 | [Fact] 274 | public async Task WaitAsync_Timeout() 275 | { 276 | var timeProvider = new FakeTimeProvider(); 277 | var source = new TaskCompletionSource(); 278 | 279 | var t = source.Task.WaitAsync(TimeSpan.FromMilliseconds(1), timeProvider, CancellationToken.None); 280 | while (!t.IsCompleted) 281 | { 282 | timeProvider.Advance(TimeSpan.FromMilliseconds(1)); 283 | await Task.Delay(1); 284 | } 285 | 286 | Assert.True(t.IsCompleted); 287 | Assert.True(t.IsFaulted); 288 | Assert.False(t.IsCanceled); 289 | } 290 | 291 | [Fact] 292 | public async Task WaitAsync_Cancel() 293 | { 294 | var timeProvider = new FakeTimeProvider(); 295 | var source = new TaskCompletionSource(); 296 | using var cts = new CancellationTokenSource(); 297 | 298 | var t = source.Task.WaitAsync(_infiniteTimeout, timeProvider, cts.Token); 299 | cts.Cancel(); 300 | 301 | #pragma warning disable VSTHRD003 // Avoid awaiting foreign Tasks 302 | await Assert.ThrowsAsync(() => t).ConfigureAwait(false); 303 | #pragma warning restore VSTHRD003 // Avoid awaiting foreign Tasks 304 | } 305 | 306 | [Fact] 307 | public void AutoAdvance() 308 | { 309 | var timeProvider = new FakeTimeProvider { AutoAdvanceBehavior = { UtcNowAdvanceAmount = TimeSpan.FromSeconds(1) } }; 310 | 311 | var first = timeProvider.GetUtcNow(); 312 | var second = timeProvider.GetUtcNow(); 313 | var third = timeProvider.GetUtcNow(); 314 | 315 | Assert.Equal(timeProvider.Start, first); 316 | Assert.Equal(timeProvider.Start + TimeSpan.FromSeconds(1), second); 317 | Assert.Equal(timeProvider.Start + TimeSpan.FromSeconds(2), third); 318 | } 319 | 320 | [Fact] 321 | public void ToString_AutoAdvance_off() 322 | { 323 | var timeProvider = new FakeTimeProvider(); 324 | 325 | _ = timeProvider.ToString(); 326 | 327 | Assert.Equal(timeProvider.Start, timeProvider.GetUtcNow()); 328 | } 329 | 330 | [Fact] 331 | public void ToString_AutoAdvance_on() 332 | { 333 | var timeProvider = new FakeTimeProvider { AutoAdvanceBehavior = { UtcNowAdvanceAmount = TimeSpan.FromSeconds(1) } }; 334 | 335 | _ = timeProvider.ToString(); 336 | 337 | timeProvider.AutoAdvanceBehavior.UtcNowAdvanceAmount = TimeSpan.Zero; 338 | Assert.Equal(timeProvider.Start, timeProvider.GetUtcNow()); 339 | } 340 | } 341 | 342 | -------------------------------------------------------------------------------- /test/TimeProviderExtensions.Tests/TimeProviderExtensions.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0;net6.0;netcoreapp3.1 5 | latest 6 | enable 7 | enable 8 | false 9 | TimeProviderExtensions 10 | true 11 | true 12 | ../../key.snk 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | runtime; build; native; contentfiles; analyzers; buildtransitive 22 | all 23 | 24 | 25 | runtime; build; native; contentfiles; analyzers; buildtransitive 26 | all 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /test/TimeProviderExtensions.Tests/Usings.cs: -------------------------------------------------------------------------------- 1 | global using System; 2 | global using System.Threading; 3 | global using FluentAssertions; 4 | global using FluentAssertions.Extensions; 5 | global using Xunit; 6 | --------------------------------------------------------------------------------