diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index f59cd067..3aa2b8ae 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -64,3 +64,4 @@ Not applicable items should be crossed out, not removed. - [ ] `CHANGELOG.md`, `README.md`, `DEVELOPER.md`, and other documentation files are updated. - [ ] Destination branch is correct - `main` or release - [ ] Create merge commit if merging release branch into `main`, squash otherwise. +- [ ] All TODOs referencing issue(s) closed by this PR have been resolved. diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index eb37145d..8e3c4c04 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -158,8 +158,12 @@ jobs: - name: Pack the client working-directory: sources/Valkey.Glide run: | - dotnet build Valkey.Glide.csproj --configuration Release "/property:Version=$RELEASE_VERSION" - dotnet pack Valkey.Glide.csproj --configuration Release "-p:NuspecProperties=version=$RELEASE_VERSION" + dotnet build Valkey.Glide.csproj \ + --configuration Release \ + "/property:Version=$RELEASE_VERSION" + dotnet pack Valkey.Glide.csproj \ + --configuration Release \ + "-p:NuspecProperties=version=$RELEASE_VERSION;commit=${{ github.sha }}" tree -h bin/Release unzip -l "bin/Release/Valkey.Glide.$RELEASE_VERSION.nupkg" unzip -l "bin/Release/Valkey.Glide.$RELEASE_VERSION.snupkg" diff --git a/.github/workflows/check-examples.yml b/.github/workflows/check-examples.yml index c516ce37..a84bf6ac 100644 --- a/.github/workflows/check-examples.yml +++ b/.github/workflows/check-examples.yml @@ -38,4 +38,4 @@ jobs: run: task build target=lib - name: Check examples - run: task check-examples + run: task check:examples diff --git a/.github/workflows/check-todos.yml b/.github/workflows/check-todos.yml new file mode 100644 index 00000000..0504b967 --- /dev/null +++ b/.github/workflows/check-todos.yml @@ -0,0 +1,34 @@ +name: check-todos + +on: + push: + branches: + - main + - release-* + pull_request: + workflow_dispatch: + +permissions: + contents: read + issues: read + +jobs: + check-todos: + runs-on: ubuntu-latest + timeout-minutes: 10 + + steps: + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + + - name: Setup Python + uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 + with: + python-version: "3.12" + + - name: Install Task + uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0 + + - name: Check TODOs + run: task check:todos + env: + GH_TOKEN: ${{ github.token }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c6380371..fb6ddfa2 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -81,8 +81,8 @@ jobs: - name: Determine whether to run with coverage shell: bash run: | - SERVER_TYPE=$(jq -r '.server_type' dev/coverage/coverage-baseline.json) - SERVER_VERSION=$(jq -r '.server_version' dev/coverage/coverage-baseline.json) + SERVER_TYPE=$(jq -r '.server_type' dev/conf/coverage-baseline.json) + SERVER_VERSION=$(jq -r '.server_version' dev/conf/coverage-baseline.json) RUN_COVERAGE=false if [[ "${{ matrix.dotnet }}" == "8.0" \ diff --git a/AGENTS.md b/AGENTS.md index e0bb496f..c9d42287 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -40,16 +40,6 @@ Common commands: - `task test coverage=true` (collect), `task coverage:report` (generate) - Coverage results go to `dev/coverage/results/`; reports to `dev/coverage/reports/` -## Lint and Format Rules (Agents) - -- Prefer `task` commands for linting and formatting. -- Lint checks (read-only, fail on issues): - - `task lint` (all checks), `task lint:rust`, `task lint:csharp`, `task lint:yaml`, `task lint:actions`, `task lint:markdown` -- Auto-fix formatting: - - `task format` (all languages), `task format:rust`, `task format:csharp`, `task format:yaml`, `task format:markdown` -- Link checking (separate from lint, slower): - - `task check-links` - ## Contribution Requirements ### Developer Certificate of Origin (DCO) Signoff @@ -159,12 +149,12 @@ Note: Conventional Commits apply to commit messages only. Do not enforce this fo ## Quality Gates (Agent Checklist) -- [ ] Build passes -- [ ] Format passes -- [ ] Lint passes -- [ ] All unit tests pass -- [ ] Relevant integration tests pass -- [ ] Documentation examples compile +- [ ] Build passes (`task build`) +- [ ] [Formatting](DEVELOPER.md#formatting) passes (`task format`) +- [ ] [Linting](DEVELOPER.md#linting) passes (`task lint`) +- [ ] [Checks](DEVELOPER.md#checks) pass (`task check`) +- [ ] All unit tests pass (`task test:unit`) +- [ ] Relevant integration tests pass (`task test:integration`) - [ ] Public API changes respect StackExchange.Redis compatibility. - [ ] All commits include DCO signoff. diff --git a/DEVELOPER.md b/DEVELOPER.md index 5df7e4e7..b5962bc3 100644 --- a/DEVELOPER.md +++ b/DEVELOPER.md @@ -250,8 +250,7 @@ task coverage:clean # Remove coverage results and reports # Linting and formatting task lint # Run all linters task format # Run all formatters -task check-links # Check for broken links - +task check # Run all checks ``` ## Advanced Testing Options @@ -339,34 +338,57 @@ To run [DNS tests](tests/Valkey.Glide.IntegrationTests/DnsTests.cs) locally: If the environment variable is not set, DNS tests will be skipped. -## Linting and Formatting +## Formatting + +Run automated formatters to ensure consistent code style. + +```bash +task format # Run all formatters +task format:rust # Run Rust formatter +task format:csharp # Run C# formatter +task format:yaml # Run YAML formatter +task format:markdown # Run Markdown formatter +``` + +## Linting + +Run linters to catch style issues and static analysis warnings. + +```bash +task lint # Run all linters +task lint:rust # Run Rust linter +task lint:csharp # Run C# linter +task lint:yaml # Run YAML linter +task lint:actions # Run GitHub Actions linter +task lint:markdown # Run Markdown linter +``` + +## Checks -Before making a contribution, ensure that all new user APIs and non-obvious code is well documented, and run the code linters and analyzers. +Run checks to validate examples, links, and TODOs. ```bash -# Run all linters: -task lint - -# Run linters for specific languages: -task lint:rust # Run Rust linting -task lint:csharp # Run C# linting -task lint:yaml # Run YAML linting -task lint:actions # Run GitHub Actions linting -task lint:markdown # Run Markdown linting - -# Run all formatters: -task format - -# Run formatters for specific languages: -task format:rust -task format:csharp -task format:yaml -task format:markdown - -# Check for broken links -task check-links +task check +task check:examples +task check:links +task check:todos ``` +### TODOs + +All TODOs must follow the format `TODO #: `, where: + +- `` is an open GitHub issue in this repository. +- `` is a short explanation of what needs to change (at least 10 characters). + +Example: + +```csharp +// TODO #472: Auto-generate this enum from the Rust source. +``` + +Files can be excluded from validation via `dev/conf/check-todos-ignore` (fnmatch glob patterns, one per line). + ## Test framework and Style The CSharp Valkey-Glide client uses xUnit v3 for testing code. The test code styles are defined in `.editorconfing` (see `dotnet_diagnostic.xUnit..` rules). The xUnit rules are enforced by the [xUnit analyzers](https://github.com/xunit/xunit.analyzers) referenced in the main xunit.v3 NuGet package. If you choose to use xunit.v3.core instead, you can reference xunit.analyzers explicitly. For additional info, please, refer to and diff --git a/Taskfile.yml b/Taskfile.yml index 0be540fe..e485f0b6 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -126,7 +126,7 @@ tasks: lint:yaml: desc: "Run YAML linting" - cmd: npx prettier --check "**/*.yml" + cmd: npx --yes prettier --check "**/*.yml" lint:actions: desc: "Run GitHub Actions linting" @@ -134,7 +134,7 @@ tasks: lint:markdown: desc: "Run Markdown linting" - cmd: npx markdownlint-cli2 "**/*.md" + cmd: npx --yes markdownlint-cli2 "**/*.md" # Format # -------------------------------------------------------------------------------- @@ -158,19 +158,30 @@ tasks: format:yaml: desc: "Run YAML formatting" - cmd: npx prettier --write --cache "**/*.yml" + cmd: npx --yes prettier --write --cache "**/*.yml" format:markdown: desc: "Run Markdown formatting" - cmd: npx markdownlint-cli2 --fix "**/*.md" + cmd: npx --yes markdownlint-cli2 --fix "**/*.md" # Additional checks # -------------------------------------------------------------------------------- - check-links: + check: + desc: "Run all checks" + deps: + - check:examples + - check:links + - check:todos + + check:examples: + desc: "Check C# examples in comments" + cmd: python3 dev/scripts/check_examples.py + + check:links: desc: "Check for broken links" cmd: lychee --config lychee.toml . - check-examples: - desc: "Check C# examples in doc comments" - cmd: python3 dev/scripts/check_examples.py + check:todos: + desc: "Check that TODOs reference an open GitHub issue" + cmd: python3 dev/scripts/check_todos.py diff --git a/dev/conf/check-todos-ignore b/dev/conf/check-todos-ignore new file mode 100644 index 00000000..42fd6bad --- /dev/null +++ b/dev/conf/check-todos-ignore @@ -0,0 +1,8 @@ +# Files excluded from the TODO checker. +# Patterns use fnmatch-style glob matching. +# Only full-line comments supported. +# See dev/scripts/check_todos.py for details. + +dev/conf/check-todos-ignore +dev/scripts/check_todos.py +DEVELOPER.md diff --git a/dev/coverage/coverage-baseline.json b/dev/conf/coverage-baseline.json similarity index 76% rename from dev/coverage/coverage-baseline.json rename to dev/conf/coverage-baseline.json index cdb285ef..6ba5958d 100644 --- a/dev/coverage/coverage-baseline.json +++ b/dev/conf/coverage-baseline.json @@ -2,5 +2,5 @@ "server_type": "valkey", "server_version": "9.0", "line_coverage": 71.4, - "branch_coverage": 52.1 + "branch_coverage": 51.9 } diff --git a/dev/coverage/.runsettings b/dev/conf/coverage-runsettings.xml similarity index 100% rename from dev/coverage/.runsettings rename to dev/conf/coverage-runsettings.xml diff --git a/dev/scripts/_constants.py b/dev/scripts/_constants.py index 83a53caf..7ff9a407 100644 --- a/dev/scripts/_constants.py +++ b/dev/scripts/_constants.py @@ -7,16 +7,20 @@ # Paths # --------------------------------------------------------------------------- +GITHUB_REPO = "valkey-io/valkey-glide-csharp" + SCRIPTS_DIR = os.path.dirname(os.path.abspath(__file__)) PROJECT_ROOT = os.path.dirname(os.path.dirname(SCRIPTS_DIR)) SOURCES_DIR = os.path.join(PROJECT_ROOT, "sources") LIBRARY_DIR = os.path.join(SOURCES_DIR, "Valkey.Glide") +CONF_DIR = os.path.join(PROJECT_ROOT, "dev", "conf") +COVERAGE_BASELINE_PATH = os.path.join(CONF_DIR, "coverage-baseline.json") +COVERAGE_RUNSETTINGS_PATH = os.path.join(CONF_DIR, "coverage-runsettings.xml") + COVERAGE_DIR = os.path.join(PROJECT_ROOT, "dev", "coverage") COVERAGE_RESULTS_DIR = os.path.join(COVERAGE_DIR, "results") COVERAGE_REPORTS_DIR = os.path.join(COVERAGE_DIR, "reports") -COVERAGE_BASELINE_PATH = os.path.join(COVERAGE_DIR, "coverage-baseline.json") -COVERAGE_RUNSETTINGS_PATH = os.path.join(COVERAGE_DIR, ".runsettings") COVERAGE_REPORTS_COMBINED_DIR = os.path.join(COVERAGE_REPORTS_DIR, "combined") COVERAGE_REPORT_COMBINED_INDEX_PATH = os.path.join( diff --git a/dev/scripts/check_examples.py b/dev/scripts/check_examples.py index 33c7b0c3..b1838a41 100644 --- a/dev/scripts/check_examples.py +++ b/dev/scripts/check_examples.py @@ -2,8 +2,11 @@ """Check code examples by extracting and compiling them. +Extracts C# code examples from XML doc comments in the source tree, then +compiles each against the built Valkey.Glide DLL to verify correctness. + Usage: - python scripts/check_examples.py + python dev/scripts/check_examples.py """ import os diff --git a/dev/scripts/check_todos.py b/dev/scripts/check_todos.py new file mode 100644 index 00000000..b228a931 --- /dev/null +++ b/dev/scripts/check_todos.py @@ -0,0 +1,162 @@ +#!/usr/bin/env python3 + +"""Check that all TODOs follow the required format. + +Validation rules: + 1. Every TODO must follow the format `TODO #: `. + 2. The description should provide a summary of the proposed changes, and must be at least 10 characters long. + 3. The referenced number must correspond to an open Valkey GLIDE C# GitHub issue. + +Usage: + python dev/scripts/check_todos.py +""" + +import os +import re +import subprocess +import sys +from fnmatch import fnmatch +from pathlib import Path +from typing import NamedTuple + +from _constants import GITHUB_REPO, PROJECT_ROOT + + +class _Todo(NamedTuple): + """A TODO found in the codebase.""" + + file: str + line: int + text: str + + +# Used by git grep to discover TODO occurrences in tracked files. +_TODO_GREP_PATTERN = r"\bTODO\b" + +# Used to validate format and extract GitHub issue ID and description. +_TODO_VALIDATION_PATTERN = re.compile( + r"TODO #(?P\d+): (?P.+)", +) + +# Minimum length for the description. +_MIN_DESCRIPTION_LENGTH = 10 + +# File path patterns to ignore. +_IGNORE_FILE = os.path.join(PROJECT_ROOT, "dev", "conf", "check-todos-ignore") + + +def _load_ignore_patterns() -> list[str]: + """Load ignore patterns from the check-todos-ignore file.""" + if not os.path.isfile(_IGNORE_FILE): + print(f"Error: ignore file not found: {_IGNORE_FILE}", file=sys.stderr) + sys.exit(1) + + return [ + ignore for line in Path(_IGNORE_FILE).read_text().splitlines() + if (ignore := line.strip()) and not ignore.startswith("#") + ] + + +def _is_ignored(filepath: str, patterns: list[str]) -> bool: + """Check if a filepath matches any exclusion pattern.""" + return any(fnmatch(filepath, p) for p in patterns) + + +def _find_todos() -> list[_Todo]: + """Find all TODOs in tracked files using git grep.""" + result = subprocess.run( + ["git", "grep", "-n", "-i", "-P", _TODO_GREP_PATTERN], + cwd=PROJECT_ROOT, + capture_output=True, + text=True, + ) + + if result.returncode == 1: + return [] + if result.returncode != 0: + print(f"Error: git grep failed: {result.stderr.strip()}", file=sys.stderr) + sys.exit(1) + + ignored_patterns = _load_ignore_patterns() + + todos = [] + for line in result.stdout.splitlines(): + parts = line.split(":", 2) + if len(parts) == 3: + filepath, line_no, text = parts + if not _is_ignored(filepath, ignored_patterns): + todos.append(_Todo(filepath, int(line_no), text.strip())) + + return todos + + +def _check_issue(github_id: int) -> str | None: + """Check issue state. Returns an error message, or None if the issue is open.""" + result = subprocess.run( + [ + "gh", "issue", "view", str(github_id), + "--repo", GITHUB_REPO, + "--json", "state", + "--jq", ".state", + ], + capture_output=True, + text=True, + ) + + if result.returncode != 0: + return f"#{github_id} is not a valid GitHub issue" + + state = result.stdout.strip() + if state == "OPEN": + return None + + return f"#{github_id} is not open (state: {state})" + + +def _validate_todos(todos: list[_Todo]) -> dict[_Todo, str]: + """Validate TODO format and issue state. Returns a map from failed TODO to the corresponding reason.""" + failures: dict[_Todo, str] = {} + checked_issues: dict[int, str | None] = {} + + for todo in todos: + + # Validate format + match = _TODO_VALIDATION_PATTERN.search(todo.text) + if not match: + failures[todo] = "invalid format (expected: TODO #: )" + continue + + # Check issue state + github_id = int(match.group("github_id")) + if github_id not in checked_issues: + checked_issues[github_id] = _check_issue(github_id) + if checked_issues[github_id]: + failures[todo] = checked_issues[github_id] + continue + + # Check description length + description = match.group("description") + if len(description.strip()) < _MIN_DESCRIPTION_LENGTH: + failures[todo] = f"description too short (must be at least {_MIN_DESCRIPTION_LENGTH} characters)" + + return failures + + +def main(): + print("Checking TODOs...\n") + + todos = _find_todos() + failures = _validate_todos(todos) + + for todo, reason in failures.items(): + print(f" FAIL {todo.file}:{todo.line}") + print(f" {reason}\n") + + passed = len(todos) - len(failures) + print(f"Checked {len(todos)} TODOs: {passed} passed, {len(failures)} failed.") + + sys.exit(1 if failures else 0) + + +if __name__ == "__main__": + main() diff --git a/dev/scripts/get_licenses_from_ort.py b/dev/scripts/get_licenses_from_ort.py index a6a77753..4fdee179 100644 --- a/dev/scripts/get_licenses_from_ort.py +++ b/dev/scripts/get_licenses_from_ort.py @@ -10,7 +10,6 @@ The script outputs a set of licenses identified by the analyzer. GLIDE maintainers should review the returned list to ensure that all licenses are approved. """ -# TODO: Modify to use logic operations instead of including AND and OR in strings APPROVED_LICENSES = [ "(Apache-2.0 OR MIT) AND Unicode-DFS-2016", "Unicode-3.0", diff --git a/docs/coverage.md b/docs/coverage.md index c0ce4556..b73bf687 100644 --- a/docs/coverage.md +++ b/docs/coverage.md @@ -2,8 +2,8 @@ This project includes support for measuring line and branch coverage, including a coverage baseline and checks to ensure coverage does not decrease. -![Line coverage](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fraw.githubusercontent.com%2Fvalkey-io%2Fvalkey-glide-csharp%2Fmain%2Fdev%2Fcoverage%2Fcoverage-baseline.json&query=%24.line_coverage&suffix=%25&label=Line%20Coverage) -![Branch coverage](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fraw.githubusercontent.com%2Fvalkey-io%2Fvalkey-glide-csharp%2Fmain%2Fdev%2Fcoverage%2Fcoverage-baseline.json&query=%24.branch_coverage&suffix=%25&label=Branch%20Coverage) +![Line coverage](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fraw.githubusercontent.com%2Fvalkey-io%2Fvalkey-glide-csharp%2Fmain%2Fdev%2Fconf%2Fcoverage-baseline.json&query=%24.line_coverage&suffix=%25&label=Line%20Coverage) +![Branch coverage](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fraw.githubusercontent.com%2Fvalkey-io%2Fvalkey-glide-csharp%2Fmain%2Fdev%2Fconf%2Fcoverage-baseline.json&query=%24.branch_coverage&suffix=%25&label=Branch%20Coverage) ## How It Works @@ -53,13 +53,15 @@ The coverage check passes only when measured coverage **exactly matches** the ba ## File Layout ```text +dev/conf/ +├── coverage-baseline.json # Coverage baseline +└── coverage-runsettings.xml # Coverlet configuration + dev/coverage/ -├── coverage-baseline.json # Coverage baseline -├── .runsettings # Coverlet configuration -├── results/ # Raw .cobertura.xml coverage results +├── results/ # Raw .cobertura.xml coverage results (gitignored) │ ├── unit/ │ └── integration/ -└── reports/ # Generated HTML/JSON reports +└── reports/ # Generated HTML/JSON reports (gitignored) ├── unit/ ├── integration/ └── combined/ @@ -67,7 +69,7 @@ dev/coverage/ ## Baseline File -The coverage baseline file (`dev/coverage/coverage-baseline.json`) stores the coverage baseline and the server configuration used to collect it. +The coverage baseline file (`dev/conf/coverage-baseline.json`) stores the coverage baseline and the server configuration used to collect it. Example: diff --git a/rust/src/ffi.rs b/rust/src/ffi.rs index 3797914b..1df9e85c 100644 --- a/rust/src/ffi.rs +++ b/rust/src/ffi.rs @@ -148,11 +148,6 @@ pub struct ConnectionConfig { pub node_discovery_mode: NodeDiscoveryMode, pub has_client_side_cache_config: bool, pub client_side_cache_config: ClientSideCacheConfig, - /* - TODO below - pub periodic_checks: Option, - pub inflight_requests_limit: Option - */ } #[repr(C)] @@ -378,8 +373,8 @@ pub(crate) unsafe fn create_connection_request( client_key_path: None, cert_reload: None, tcp_nodelay: false, - periodic_checks: None, - inflight_requests_limit: None, + periodic_checks: None, // TODO #485: Expose periodic_checks in ClusterClientConfiguration. + inflight_requests_limit: None, // TODO #484: Expose inflight_requests_limit in ConnectionConfiguration. address_resolver: None, client_circuit_breaker: None, }) diff --git a/sources/Valkey.Glide/Abstract/ConnectionMultiplexer.cs b/sources/Valkey.Glide/Abstract/ConnectionMultiplexer.cs index 8555e5f3..cd8e3fc4 100644 --- a/sources/Valkey.Glide/Abstract/ConnectionMultiplexer.cs +++ b/sources/Valkey.Glide/Abstract/ConnectionMultiplexer.cs @@ -121,8 +121,6 @@ public IServer GetServer(EndPoint endpoint) throw new ArgumentException("The specified endpoint is not defined", nameof(endpoint)); } - // TODO currently this returns only primary node on standalone - // https://github.com/valkey-io/valkey-glide/issues/4293 /// public IServer[] GetServers() { @@ -134,7 +132,7 @@ public IServer[] GetServers() } else { - // due to #4293, core ignores route on standalone and always return a single node response + // glide-core ignores route on standalone and always returns a single node response string info = _db.Command(Request.Info([]), Route.AllNodes).GetAwaiter().GetResult(); // and there is no way to get IP address from server, assuming localhost (127.0.0.1) // we can try to get port only (in some deployments, this info is also missing) @@ -375,7 +373,9 @@ internal void RemoveAllSubscriptions() lock (_subscriptions) { foreach (var sub in _subscriptions.Values) + { sub.Dispose(); + } _subscriptions.Clear(); } diff --git a/sources/Valkey.Glide/Abstract/Database.SetCommands.cs b/sources/Valkey.Glide/Abstract/Database.SetCommands.cs index 202a6ca0..9dba9ad7 100644 --- a/sources/Valkey.Glide/Abstract/Database.SetCommands.cs +++ b/sources/Valkey.Glide/Abstract/Database.SetCommands.cs @@ -138,47 +138,19 @@ public Task SetMoveAsync(ValkeyKey source, ValkeyKey destination, ValkeyVa } /// - public IAsyncEnumerable SetScanAsync(ValkeyKey key, ValkeyValue pattern = default, int pageSize = 250, long cursor = 0, int pageOffset = 0, CommandFlags flags = CommandFlags.None) + public IAsyncEnumerable SetScanAsync( + ValkeyKey key, + ValkeyValue pattern = default, + int pageSize = 250, + long cursor = 0, + int pageOffset = 0, + CommandFlags flags = CommandFlags.None) { GuardClauses.ThrowIfCommandFlags(flags); + GuardClauses.ThrowIfNotPositive(pageSize, nameof(pageSize)); - // Build ScanOptions from the SER parameters - ScanOptions? options = null; - if (!pattern.IsNull || pageSize != 250) - { - options = new ScanOptions(); - if (!pattern.IsNull) - { - options.MatchPattern = pattern; - } - if (pageSize != 250) - { - options.Count = pageSize; - } - } - - return SetScanWithOffsetAsync(key, options, cursor, pageOffset); - } - - private async IAsyncEnumerable SetScanWithOffsetAsync(ValkeyKey key, ScanOptions? options, long cursor, int pageOffset) - { - long currentCursor = cursor; - int currentOffset = pageOffset; - - do - { - (long nextCursor, ValkeyValue[] elements) = await Command(Request.SetScanAsync(key, currentCursor, options)); - - IEnumerable elementsToYield = currentOffset > 0 ? elements.Skip(currentOffset) : elements; - - foreach (ValkeyValue element in elementsToYield) - { - yield return element; - } - - currentCursor = nextCursor; - currentOffset = 0; // Only skip on first iteration - } while (currentCursor != 0); + var options = new ScanOptions { MatchPattern = pattern, Count = pageSize }; + return SetScanAsync(key, cursor, options).SkipAsync(pageOffset); } private async Task> GetCombineResultAsync(SetOperation operation, IEnumerable keys) => operation switch diff --git a/sources/Valkey.Glide/Abstract/Database.SortedSetCommands.cs b/sources/Valkey.Glide/Abstract/Database.SortedSetCommands.cs index 7b53c715..a27f1f2c 100644 --- a/sources/Valkey.Glide/Abstract/Database.SortedSetCommands.cs +++ b/sources/Valkey.Glide/Abstract/Database.SortedSetCommands.cs @@ -291,48 +291,19 @@ public Task SortedSetRemoveRangeByScoreAsync(ValkeyKey key, double start, } /// - public IAsyncEnumerable SortedSetScanAsync(ValkeyKey key, ValkeyValue pattern = default, int pageSize = 250, long cursor = 0, int pageOffset = 0, CommandFlags flags = CommandFlags.None) + public IAsyncEnumerable SortedSetScanAsync( + ValkeyKey key, + ValkeyValue pattern = default, + int pageSize = 250, + long cursor = 0, + int pageOffset = 0, + CommandFlags flags = CommandFlags.None) { GuardClauses.ThrowIfCommandFlags(flags); + GuardClauses.ThrowIfNotPositive(pageSize, nameof(pageSize)); - // TODO - Extract common logic for building ScanOptions from SER arguments. - // Build ScanOptions from the SER parameters - ScanOptions? options = null; - if (!pattern.IsNull || pageSize != 250) - { - options = new ScanOptions(); - if (!pattern.IsNull) - { - options.MatchPattern = pattern; - } - if (pageSize != 250) - { - options.Count = pageSize; - } - } - - return SortedSetScanWithOffsetAsync(key, options, cursor, pageOffset); - } - - private async IAsyncEnumerable SortedSetScanWithOffsetAsync(ValkeyKey key, ScanOptions? options, long cursor, int pageOffset) - { - long currentCursor = cursor; - int currentOffset = pageOffset; - - do - { - (long nextCursor, SortedSetEntry[] elements) = await Command(Request.SortedSetScanAsync(key, currentCursor, options)); - - IEnumerable elementsToYield = currentOffset > 0 ? elements.Skip(currentOffset) : elements; - - foreach (SortedSetEntry element in elementsToYield) - { - yield return element; - } - - currentCursor = nextCursor; - currentOffset = 0; // Only skip on first iteration - } while (currentCursor != 0); + var options = new ScanOptions { MatchPattern = pattern, Count = pageSize }; + return SortedSetScanAsync(key, cursor, options).SkipAsync(pageOffset); } /// diff --git a/sources/Valkey.Glide/Abstract/Database.StreamCommands.cs b/sources/Valkey.Glide/Abstract/Database.StreamCommands.cs index 2e6719c4..590470c8 100644 --- a/sources/Valkey.Glide/Abstract/Database.StreamCommands.cs +++ b/sources/Valkey.Glide/Abstract/Database.StreamCommands.cs @@ -202,23 +202,48 @@ public Task StreamAutoClaimIdsOnlyAsync(ValkeyKey k public Task StreamTrimAsync(ValkeyKey key, int maxLength, bool useApproximateMaxLength = false, CommandFlags flags = CommandFlags.None) { GuardClauses.ThrowIfCommandFlags(flags); - return Command(Request.StreamTrimAsync(key, maxLength, default, useApproximateMaxLength, null)); + GuardClauses.ThrowIfNegative(maxLength, nameof(maxLength)); + + return StreamTrimAsync(key, new StreamTrimOptions.MaxLen + { + MaxLength = maxLength, + Exact = !useApproximateMaxLength, + }); } /// public Task StreamTrimAsync(ValkeyKey key, long? maxLength = null, bool useApproximateMaxLength = false, long? limit = null, StreamTrimMode trimMode = StreamTrimMode.KeepReferences, CommandFlags flags = CommandFlags.None) { GuardClauses.ThrowIfCommandFlags(flags); - ThrowIfUnsupportedTrimMode(trimMode); - return Command(Request.StreamTrimAsync(key, maxLength, default, useApproximateMaxLength, limit)); + GuardClauses.ThrowIfNotSupported(trimMode); + + // TODO #486: Remove once maxLength is no longer optional. + ArgumentNullException.ThrowIfNull(maxLength, nameof(maxLength)); + + var options = new StreamTrimOptions.MaxLen + { + MaxLength = maxLength.Value, + Exact = !useApproximateMaxLength, + Limit = limit + }; + + return StreamTrimAsync(key, options); } /// public Task StreamTrimByMinIdAsync(ValkeyKey key, ValkeyValue minId, bool useApproximateMaxLength = false, long? limit = null, StreamTrimMode trimMode = StreamTrimMode.KeepReferences, CommandFlags flags = CommandFlags.None) { GuardClauses.ThrowIfCommandFlags(flags); - ThrowIfUnsupportedTrimMode(trimMode); - return Command(Request.StreamTrimAsync(key, null, minId, useApproximateMaxLength, limit)); + GuardClauses.ThrowIfNotSupported(trimMode); + + var options = new StreamTrimOptions.MinId + { + MinEntryId = minId, + Exact = !useApproximateMaxLength, + Limit = limit + }; + + return StreamTrimAsync(key, options); } /// @@ -248,26 +273,18 @@ public Task StreamConsumerInfoAsync(ValkeyKey key, ValkeyV /// /// Converts the given arguments to a instance. /// - private static StreamAddOptions ToStreamAddOptions(ValkeyValue? messageId, long? maxLength, bool useApproximateMaxLength, long? limit = null) => new() + private static StreamAddOptions ToStreamAddOptions( + ValkeyValue? messageId, + long? maxLength, + bool useApproximateMaxLength, + long? limit = null) + => new() { Id = messageId ?? StreamAddOptions.AutoGenerateId, Trim = maxLength.HasValue - ? new StreamTrimOptions.MaxLen { MaxLength = maxLength.Value, Exact = !useApproximateMaxLength, Limit = limit } - : null + ? new StreamTrimOptions.MaxLen { MaxLength = maxLength.Value, Exact = !useApproximateMaxLength, Limit = limit } + : null }; - /// - /// Throws a if the stream trim mode is not supported. - /// - /// The stream trim mode to validate. - /// Thrown if is not . - private static void ThrowIfUnsupportedTrimMode(StreamTrimMode trimMode) - { - if (trimMode != StreamTrimMode.KeepReferences) - { - throw new NotImplementedException($"Stream trim mode {trimMode} is not supported by Valkey GLIDE"); - } - } - #endregion } diff --git a/sources/Valkey.Glide/Abstract/IDatabaseAsync.SetCommands.cs b/sources/Valkey.Glide/Abstract/IDatabaseAsync.SetCommands.cs index 5a2b22b5..bff67451 100644 --- a/sources/Valkey.Glide/Abstract/IDatabaseAsync.SetCommands.cs +++ b/sources/Valkey.Glide/Abstract/IDatabaseAsync.SetCommands.cs @@ -186,11 +186,14 @@ public partial interface IDatabaseAsync /// The pattern to match. /// The number of elements to return per iteration (hint to the server). /// The cursor position to start at. - /// The number of elements to skip from the first page. + /// The number of elements to skip from the beginning of the result stream. /// Command flags (currently not supported by GLIDE). /// An that yields all matching elements of the set. /// Thrown if is not . /// + /// Unlike StackExchange.Redis, which only skips elements within the first scan batch (making the + /// skip non-deterministic since batch sizes are server-controlled), GLIDE skips + /// elements from the overall result stream regardless of internal batching. This provides deterministic behavior. /// /// /// await db.SetAddAsync("myset", ["apple", "banana", "apricot"]); diff --git a/sources/Valkey.Glide/Abstract/IDatabaseAsync.SortedSetCommands.cs b/sources/Valkey.Glide/Abstract/IDatabaseAsync.SortedSetCommands.cs index 66fcb22e..b3e97c0b 100644 --- a/sources/Valkey.Glide/Abstract/IDatabaseAsync.SortedSetCommands.cs +++ b/sources/Valkey.Glide/Abstract/IDatabaseAsync.SortedSetCommands.cs @@ -285,10 +285,15 @@ public partial interface IDatabaseAsync /// The pattern to match. /// The number of elements to return per iteration (hint to the server). /// The cursor position to start at. - /// The number of elements to skip from the first page. + /// The number of elements to skip from the beginning of the result stream. /// Command flags (currently not supported by GLIDE). /// An that yields all matching elements of the sorted set. /// Thrown if is not . + /// + /// Unlike StackExchange.Redis, which only skips elements within the first scan batch (making the + /// skip non-deterministic since batch sizes are server-controlled), GLIDE skips + /// elements from the overall result stream regardless of internal batching. This provides deterministic behavior. + /// IAsyncEnumerable SortedSetScanAsync(ValkeyKey key, ValkeyValue pattern = default, int pageSize = 250, long cursor = 0, int pageOffset = 0, CommandFlags flags = CommandFlags.None); /// diff --git a/sources/Valkey.Glide/Abstract/IDatabaseAsync.StreamCommands.cs b/sources/Valkey.Glide/Abstract/IDatabaseAsync.StreamCommands.cs index a54fccf9..16737371 100644 --- a/sources/Valkey.Glide/Abstract/IDatabaseAsync.StreamCommands.cs +++ b/sources/Valkey.Glide/Abstract/IDatabaseAsync.StreamCommands.cs @@ -344,14 +344,35 @@ public partial interface IDatabaseAsync /// Command flags (currently not supported by GLIDE). /// The number of entries removed. /// Thrown if is not . - Task StreamTrimAsync(ValkeyKey key, int maxLength, bool useApproximateMaxLength = false, CommandFlags flags = CommandFlags.None); + /// + /// The default and + /// values on this overload will be removed to match StackExchange.Redis. + /// See #486. + /// + // TODO #486: Remove default parameter values to match StackExchange.Redis signature in 2.0. + Task StreamTrimAsync( + ValkeyKey key, + int maxLength, + bool useApproximateMaxLength = false, + CommandFlags flags = CommandFlags.None); /// - /// The maximum number of entries to keep. + /// The maximum number of entries to keep. Must not be null. /// The maximum number of entries to evict per call. /// The trim mode to use. /// The number of entries removed. - Task StreamTrimAsync(ValkeyKey key, long? maxLength = null, bool useApproximateMaxLength = false, long? limit = null, StreamTrimMode trimMode = StreamTrimMode.KeepReferences, CommandFlags flags = CommandFlags.None); + /// + /// The parameter will change from long? to long + /// match StackExchange.Redis. See #486. + /// + // TODO #486: Change `long? maxLength = null` to `long maxLength` to match StackExchange.Redis signature in 2.0. + Task StreamTrimAsync( + ValkeyKey key, + long? maxLength = null, + bool useApproximateMaxLength = false, + long? limit = null, + StreamTrimMode trimMode = StreamTrimMode.KeepReferences, + CommandFlags flags = CommandFlags.None); #endregion #region StreamTrimByMinIdAsync diff --git a/sources/Valkey.Glide/Abstract/IServer.cs b/sources/Valkey.Glide/Abstract/IServer.cs index 15080694..fce9ceab 100644 --- a/sources/Valkey.Glide/Abstract/IServer.cs +++ b/sources/Valkey.Glide/Abstract/IServer.cs @@ -349,13 +349,20 @@ public partial interface IServer : IRedisAsync /// The pattern to match keys against. If not specified, all keys are returned. /// The number of keys to return per SCAN iteration. /// The cursor to start scanning from. - /// The number of keys to skip from the first page of results. + /// The number of keys to skip from the beginning of the result stream. /// Command flags (currently not supported by GLIDE). /// Thrown if is not . /// An async enumerable of keys matching the pattern. /// + /// /// Unlike StackExchange.Redis, GLIDE does not support per-database selection. /// Use to change the current database. + /// + /// + /// Unlike StackExchange.Redis, which only skips elements within the first scan batch (making the + /// skip non-deterministic since batch sizes are server-controlled), GLIDE skips + /// elements from the overall result stream regardless of internal batching. This provides deterministic behavior. + /// /// IAsyncEnumerable KeysAsync( ValkeyValue pattern = default, diff --git a/sources/Valkey.Glide/Abstract/ValkeyServer.cs b/sources/Valkey.Glide/Abstract/ValkeyServer.cs index bba0d5c1..71a139f8 100644 --- a/sources/Valkey.Glide/Abstract/ValkeyServer.cs +++ b/sources/Valkey.Glide/Abstract/ValkeyServer.cs @@ -282,42 +282,18 @@ public async Task ScriptFlushAsync(CommandFlags flags = CommandFlags.None) } /// - public async IAsyncEnumerable KeysAsync(ValkeyValue pattern = default, int pageSize = 250, long cursor = 0, int pageOffset = 0, CommandFlags flags = CommandFlags.None) + public IAsyncEnumerable KeysAsync( + ValkeyValue pattern = default, + int pageSize = 250, + long cursor = 0, + int pageOffset = 0, + CommandFlags flags = CommandFlags.None) { GuardClauses.ThrowIfCommandFlags(flags); + GuardClauses.ThrowIfNotPositive(pageSize, nameof(pageSize)); - var options = new ScanOptions(); - if (!pattern.IsNull) - { - options.MatchPattern = pattern; - } - - // TODO how to handle negative - if (pageSize > 0) - { - options.Count = pageSize; - } - - string currentCursor = cursor.ToString(); - ValkeyKey[] keys; - int currentOffset = pageOffset; - - do - { - (currentCursor, keys) = await _conn.Command(Request.ScanAsync(currentCursor, options)); - - if (currentOffset > 0) - { - keys = [.. keys.Skip(currentOffset)]; - currentOffset = 0; - } - - foreach (ValkeyKey key in keys) - { - yield return key; - } - - } while (currentCursor != "0"); + var options = new ScanOptions { MatchPattern = pattern, Count = pageSize }; + return ScanAsync(cursor.ToString(), options).SkipAsync(pageOffset); } /// @@ -345,5 +321,19 @@ private Route MakeRoute() return new ByAddressRoute(host, port); } + private async IAsyncEnumerable ScanAsync(string cursor, ScanOptions options) + { + var route = MakeRoute(); + + do + { + (cursor, ValkeyKey[] keys) = await _conn.Command(Request.ScanAsync(cursor, options), route); + foreach (ValkeyKey key in keys) + { + yield return key; + } + } while (cursor != "0"); + } + #endregion } diff --git a/sources/Valkey.Glide/Client/BaseClient.SetCommands.cs b/sources/Valkey.Glide/Client/BaseClient.SetCommands.cs index c9e85e25..fa453fe4 100644 --- a/sources/Valkey.Glide/Client/BaseClient.SetCommands.cs +++ b/sources/Valkey.Glide/Client/BaseClient.SetCommands.cs @@ -8,6 +8,8 @@ namespace Valkey.Glide; public abstract partial class BaseClient { + #region Public Methods + /// public async Task SetAddAsync(ValkeyKey key, ValkeyValue value) => await Command(Request.SetAddAsync(key, value)); @@ -88,22 +90,29 @@ public async Task SetRandomMembersAsync(ValkeyKey key, long count public async Task SetMoveAsync(ValkeyKey source, ValkeyKey destination, ValkeyValue value) => await Command(Request.SetMoveAsync(source, destination, value)); - // TODO #287 /// - public async IAsyncEnumerable SetScanAsync(ValkeyKey key, ScanOptions? options = null) - { - long currentCursor = 0; + public IAsyncEnumerable SetScanAsync(ValkeyKey key, ScanOptions? options = null) + => SetScanAsync(key, 0, options); + #endregion + #region Internal Methods + + /// + private protected async IAsyncEnumerable SetScanAsync( + ValkeyKey key, + long cursor, + ScanOptions? options) + { do { - (long nextCursor, ValkeyValue[] elements) = await Command(Request.SetScanAsync(key, currentCursor, options)); - - foreach (ValkeyValue element in elements) + (cursor, var elements) = await Command(Request.SetScanAsync(key, cursor, options)); + foreach (var element in elements) { yield return element; } - currentCursor = nextCursor; - } while (currentCursor != 0); + } while (cursor != 0); } + + #endregion } diff --git a/sources/Valkey.Glide/Client/BaseClient.SortedSetCommands.cs b/sources/Valkey.Glide/Client/BaseClient.SortedSetCommands.cs index c8c6c83a..272528b5 100644 --- a/sources/Valkey.Glide/Client/BaseClient.SortedSetCommands.cs +++ b/sources/Valkey.Glide/Client/BaseClient.SortedSetCommands.cs @@ -8,6 +8,8 @@ namespace Valkey.Glide; public abstract partial class BaseClient { + #region Public Methods + /// public Task SortedSetAddAsync(ValkeyKey key, ValkeyValue member, double score) => Command(Request.SortedSetAddAsync(key, member, score)); @@ -248,22 +250,29 @@ public async Task SortedSetBlockingPopAsync(IEnumerable - public async IAsyncEnumerable SortedSetScanAsync(ValkeyKey key, ScanOptions? options = null) - { - long currentCursor = 0; + public IAsyncEnumerable SortedSetScanAsync(ValkeyKey key, ScanOptions? options = null) + => SortedSetScanAsync(key, 0, options); + #endregion + #region Protected Methods + + /// + private protected async IAsyncEnumerable SortedSetScanAsync( + ValkeyKey key, + long cursor, + ScanOptions? options) + { do { - (long nextCursor, SortedSetEntry[] elements) = await Command(Request.SortedSetScanAsync(key, currentCursor, options)); - - foreach (SortedSetEntry element in elements) + (cursor, var elements) = await Command(Request.SortedSetScanAsync(key, cursor, options)); + foreach (var element in elements) { yield return element; } - currentCursor = nextCursor; - } while (currentCursor != 0); + } while (cursor != 0); } + + #endregion } diff --git a/sources/Valkey.Glide/Client/BaseClient.StreamCommands.cs b/sources/Valkey.Glide/Client/BaseClient.StreamCommands.cs index 9efe805d..767c3aed 100644 --- a/sources/Valkey.Glide/Client/BaseClient.StreamCommands.cs +++ b/sources/Valkey.Glide/Client/BaseClient.StreamCommands.cs @@ -95,7 +95,8 @@ public Task StreamDeleteAsync(ValkeyKey key, ValkeyValue messageId) #endregion - // TODO #326 + // TODO #326: Redesign and expose these methods + // ────────────────────────────────────────────────────────────────────── // Methods below are temporarily commented out pending cleanup. // The underlying Request methods are kept intact. @@ -166,16 +167,13 @@ public Task StreamDeleteAsync(ValkeyKey key, ValkeyValue messageId) // => await Command(Request.StreamAutoClaimJustIdAsync(key, consumerGroup, claimingConsumer, minIdleTime, startAtId, count)); // #endregion - // #region XTRIM - // public async Task StreamTrimAsync(ValkeyKey key, int maxLength, bool useApproximateMaxLength) - // => await StreamTrimAsync(key, maxLength, useApproximateMaxLength, null); - // - // public async Task StreamTrimAsync(ValkeyKey key, long? maxLength = null, bool useApproximateMaxLength = false, long? limit = null) - // => await Command(Request.StreamTrimAsync(key, maxLength, default, useApproximateMaxLength, limit)); - // - // public async Task StreamTrimByMinIdAsync(ValkeyKey key, ValkeyValue minId, bool useApproximateMaxLength = false, long? limit = null) - // => await Command(Request.StreamTrimAsync(key, null, minId, useApproximateMaxLength, limit)); - // #endregion + #region StreamTrimAsync + + /// + public Task StreamTrimAsync(ValkeyKey key, StreamTrimOptions options) + => Command(Request.StreamTrimAsync(key, options)); + + #endregion // #region XINFO // public async Task StreamInfoAsync(ValkeyKey key) diff --git a/sources/Valkey.Glide/Client/GlideClient.GenericCommands.cs b/sources/Valkey.Glide/Client/GlideClient.GenericCommands.cs index ae6a4fab..8de9de55 100644 --- a/sources/Valkey.Glide/Client/GlideClient.GenericCommands.cs +++ b/sources/Valkey.Glide/Client/GlideClient.GenericCommands.cs @@ -17,21 +17,20 @@ public async Task MigrateAsync(IEnumerable keys, MigrateOptions => await Command(Request.ScanAsync(cursor, options)); /// - public async IAsyncEnumerable ScanAsync(ScanOptions? options = null) - { - string currentCursor = "0"; + public IAsyncEnumerable ScanAsync(ScanOptions? options = null) + => ScanKeysAsync("0", options); + /// + internal async IAsyncEnumerable ScanKeysAsync(string cursor, ScanOptions? options) + { do { - (string nextCursor, ValkeyKey[] keys) = await Command(Request.ScanAsync(currentCursor, options)); - - foreach (ValkeyKey key in keys) + (cursor, var keys) = await Command(Request.ScanAsync(cursor, options)); + foreach (var key in keys) { yield return key; } - - currentCursor = nextCursor; - } while (currentCursor != "0"); + } while (cursor != "0"); } /// diff --git a/sources/Valkey.Glide/Client/GlideClient.cs b/sources/Valkey.Glide/Client/GlideClient.cs index 1bbcfdfa..5dfe5862 100644 --- a/sources/Valkey.Glide/Client/GlideClient.cs +++ b/sources/Valkey.Glide/Client/GlideClient.cs @@ -18,42 +18,12 @@ public partial class GlideClient : BaseClient, IGlideClient { - internal GlideClient() { } + #region Public Methods - // TODO add pubsub and other params to example and remarks /// /// Creates a new instance and establishes a connection to a standalone Valkey server. /// - /// - /// Remarks: - /// Use this static method to create and connect a to a standalone Valkey server.
- /// The client will automatically handle connection establishment, including any authentication and TLS configurations. - /// - /// - /// Authentication: If credentials are provided, the client will attempt to authenticate using the specified username and password. - /// - /// - /// TLS: If is set to , the client will establish a secure connection using TLS. - /// - /// - /// Reconnection Strategy: The settings define how the client will attempt to reconnect in case of disconnections. - /// - /// - /// - /// - /// var config = new ConnectionConfiguration.StandaloneClientConfigurationBuilder() - /// .WithAddress("primary.example.com", 6379) - /// .WithAddress("replica1.example.com", 6379) - /// .WithDatabaseId(1) - /// .WithAuthentication("user1", "passwordA") - /// .WithTls() - /// .WithConnectionRetryStrategy(5, 100, 2) - /// .Build(); - /// await using var client = await GlideClient.CreateClient(config); - /// - /// - ///
- /// The configuration options for the client, including server addresses, authentication credentials, TLS settings, database selection, reconnection strategy, and Pub/Sub subscriptions. + /// The configuration options for the client. /// A task that resolves to a connected instance. public static async Task CreateClient(StandaloneClientConfiguration config) => await CreateClient(config, () => new GlideClient()); @@ -88,4 +58,6 @@ protected override async Task GetServerVersionAsync() return _serverVersion; } + + #endregion } diff --git a/sources/Valkey.Glide/Client/GlideClusterClient.GenericCommands.cs b/sources/Valkey.Glide/Client/GlideClusterClient.GenericCommands.cs index ba0e3557..60723be8 100644 --- a/sources/Valkey.Glide/Client/GlideClusterClient.GenericCommands.cs +++ b/sources/Valkey.Glide/Client/GlideClusterClient.GenericCommands.cs @@ -13,16 +13,12 @@ public partial class GlideClusterClient /// public async IAsyncEnumerable ScanAsync(ScanOptions? options = null) { - List glideArgs = []; - glideArgs.AddRange(Request.ToScanArgs(options)); - string[] args = [.. glideArgs.Select(a => a.ToString())]; - ClusterScanCursor cursor = ClusterScanCursor.InitialCursor(); + var cursor = ClusterScanCursor.InitialCursor(); + var args = Request.ToScanArgs(options).Select(a => a.ToString()).ToArray(); while (!cursor.IsFinished) { - var (nextCursorId, keys) = await ClusterScanCommand(cursor.CursorId, args); - cursor = new ClusterScanCursor(nextCursorId); - + (cursor, var keys) = await ClusterScanCommand(cursor, args); foreach (ValkeyKey key in keys) { yield return key; diff --git a/sources/Valkey.Glide/Client/GlideClusterClient.cs b/sources/Valkey.Glide/Client/GlideClusterClient.cs index c510fb63..5a350378 100644 --- a/sources/Valkey.Glide/Client/GlideClusterClient.cs +++ b/sources/Valkey.Glide/Client/GlideClusterClient.cs @@ -26,36 +26,11 @@ public sealed partial class GlideClusterClient : { private GlideClusterClient() { } - // TODO add pubsub and other params to example and remarks /// /// Creates a new instance and establishes a connection to a cluster of Valkey servers.
///
- /// - /// Remarks: - /// Use this static method to create and connect a to a Valkey Cluster.
- /// The client will automatically handle connection establishment, including cluster topology discovery and handling of authentication and TLS configurations. - /// - /// - /// Authentication: If credentials are provided, the client will attempt to authenticate using the specified username and password. - /// - /// - /// TLS: If is set to , the client will establish a secure connection using TLS. - /// - /// - /// - /// - /// var config = new ConnectionConfiguration.ClusterClientConfigurationBuilder() - /// .WithAddress("address1.example.com", 6379) - /// .WithAddress("address2.example.com", 6379) - /// .WithAuthentication("user1", "passwordA") - /// .WithTls() - /// .Build(); - /// await using var client = await GlideClusterClient.CreateClient(config); - /// - /// - ///
- /// The configuration options for the client, including cluster addresses, authentication credentials, TLS settings, periodic checks, and Pub/Sub subscriptions. - /// A task that resolves to a connected instance. + /// The configuration options for the client. + /// A task that resolves to a connected instance. public static async Task CreateClient(ClusterClientConfiguration config) => await CreateClient(config, () => new GlideClusterClient()); @@ -102,10 +77,10 @@ protected override async Task GetServerVersionAsync() /// The cursor for the scan iteration. /// Additional arguments for the scan command. /// A tuple containing the next cursor and the keys found in this iteration. - private async Task<(string cursor, ValkeyKey[] keys)> ClusterScanCommand(string cursor, string[] args) + private async Task<(ClusterScanCursor cursor, ValkeyKey[] keys)> ClusterScanCommand(ClusterScanCursor cursor, string[] args) { var message = MessageContainer.GetMessageForCall(); - IntPtr cursorPtr = Marshal.StringToHGlobalAnsi(cursor); + IntPtr cursorPtr = Marshal.StringToHGlobalAnsi(cursor.CursorId); IntPtr[]? argPtrs = null; IntPtr argsPtr = IntPtr.Zero; @@ -140,8 +115,8 @@ protected override async Task GetServerVersionAsync() { var result = HandleResponse(response); var array = (object[])result!; - var nextCursor = array[0]!.ToString()!; - var keys = ((object[])array[1]!).Select(k => new ValkeyKey(k!.ToString())).ToArray(); + var nextCursor = new ClusterScanCursor(array[0]!.ToString()!); + var keys = ((object[])array[1]!).Cast().Select(gs => (ValkeyKey)gs.Bytes).ToArray(); return (nextCursor, keys); } finally diff --git a/sources/Valkey.Glide/Client/IBaseClient.StreamCommands.cs b/sources/Valkey.Glide/Client/IBaseClient.StreamCommands.cs index 577013fa..f09e48cf 100644 --- a/sources/Valkey.Glide/Client/IBaseClient.StreamCommands.cs +++ b/sources/Valkey.Glide/Client/IBaseClient.StreamCommands.cs @@ -249,10 +249,18 @@ public partial interface IBaseClient // Task StreamConsumerGroupSetPositionAsync(ValkeyKey key, ValkeyValue groupName, ValkeyValue position, long? entriesRead = null); // #endregion - // #region XTRIM - // Task StreamTrimAsync(ValkeyKey key, long? maxLength = null, bool useApproximateMaxLength = false, long? limit = null); - // Task StreamTrimByMinIdAsync(ValkeyKey key, ValkeyValue minId, bool useApproximateMaxLength = false, long? limit = null); - // #endregion + #region StreamTrimAsync + + /// + /// Trims the stream stored at . + /// + /// The key of the stream. + /// The stream trim options. + /// The number of entries removed from the stream. + /// Valkey commands - XTRIM + Task StreamTrimAsync(ValkeyKey key, StreamTrimOptions options); + + #endregion // #region XCLAIM // Task StreamClaimAsync(ValkeyKey key, ValkeyValue consumerGroup, ValkeyValue claimingConsumer, TimeSpan minIdleTime, IEnumerable messageIds, StreamClaimOptions options); diff --git a/sources/Valkey.Glide/Commands/Options/ScanOptions.cs b/sources/Valkey.Glide/Commands/Options/ScanOptions.cs index ecd4e755..905ff72f 100644 --- a/sources/Valkey.Glide/Commands/Options/ScanOptions.cs +++ b/sources/Valkey.Glide/Commands/Options/ScanOptions.cs @@ -5,8 +5,13 @@ namespace Valkey.Glide.Commands.Options; /// /// Options for the scan commands. /// +/// Valkey commands - SCAN +/// Valkey commands - SSCAN +/// Valkey commands - ZSCAN public class ScanOptions { + #region Public Properties + /// /// Pattern to filter keys against. /// @@ -21,4 +26,51 @@ public class ScanOptions /// Type to filter keys against. /// public ValkeyType? Type { get; set; } + + #endregion + #region Internal Methods + + /// + /// Converts the options to command arguments. + /// + internal GlideString[] ToArgs() + { + List args = []; + + if (!MatchPattern.IsNull) + { + args.Add(ValkeyLiterals.MATCH); + args.Add(MatchPattern.ToGlideString()); + } + + if (Count.HasValue) + { + args.Add(ValkeyLiterals.COUNT); + args.Add(Count.Value.ToGlideString()); + } + + if (Type.HasValue) + { + args.Add(ValkeyLiterals.TYPE); + args.Add(ToType(Type.Value)); + } + + return [.. args]; + } + + /// + /// Converts the ValkeyType enum to a string. + /// + private static GlideString ToType(ValkeyType type) => type switch + { + ValkeyType.String => "string", + ValkeyType.List => "list", + ValkeyType.Set => "set", + ValkeyType.SortedSet => "zset", + ValkeyType.Hash => "hash", + ValkeyType.Stream => "stream", + ValkeyType.Unknown or ValkeyType.None or _ => throw new ArgumentException($"Unsupported ValkeyType '{type}'") + }; + + #endregion } diff --git a/sources/Valkey.Glide/Errors.cs b/sources/Valkey.Glide/Errors.cs index 941db9c6..26dfc3e6 100644 --- a/sources/Valkey.Glide/Errors.cs +++ b/sources/Valkey.Glide/Errors.cs @@ -164,8 +164,6 @@ public ConnectionException(string message, Exception innerException) : base(mess /// public sealed class ConfigurationError : GlideException { - // TODO set HelpLink with link to wiki - /// /// Initializes a new instance of the class. /// diff --git a/sources/Valkey.Glide/GlideString.cs b/sources/Valkey.Glide/GlideString.cs index 1b611c88..ed50e2db 100644 --- a/sources/Valkey.Glide/GlideString.cs +++ b/sources/Valkey.Glide/GlideString.cs @@ -291,12 +291,9 @@ public bool CanConvertToString() return (bool)_canConvertToString; } - // TODO find a better way to check this - // Detect whether `bytes` could be represented by a UTF-8 string without data corruption - string tmpStr = Encoding.UTF8.GetString(Bytes); - if (Encoding.UTF8.GetBytes(tmpStr).SequenceEqual(Bytes)) + if (System.Text.Unicode.Utf8.IsValid(Bytes)) { - Str = tmpStr; + Str = Encoding.UTF8.GetString(Bytes); _canConvertToString = true; return true; } diff --git a/sources/Valkey.Glide/Internals/FFI.structs.cs b/sources/Valkey.Glide/Internals/FFI.structs.cs index 48d8919f..d7e08153 100644 --- a/sources/Valkey.Glide/Internals/FFI.structs.cs +++ b/sources/Valkey.Glide/Internals/FFI.structs.cs @@ -609,7 +609,7 @@ private struct BatchOptionsInfo public IntPtr Route; } - // TODO: generate this with a bindings generator + // TODO #472: Auto-generate this enum internal enum RequestType : int { /// Invalid request type @@ -1144,8 +1144,6 @@ private struct ConnectionRequest [MarshalAs(UnmanagedType.U1)] public bool HasClientSideCacheConfig; public ClientSideCacheConfig ClientSideCacheConfig; - - // TODO more config params, see ffi.rs } [StructLayout(LayoutKind.Sequential)] diff --git a/sources/Valkey.Glide/Internals/GuardClauses.cs b/sources/Valkey.Glide/Internals/GuardClauses.cs index 603e437a..7eb79b2b 100644 --- a/sources/Valkey.Glide/Internals/GuardClauses.cs +++ b/sources/Valkey.Glide/Internals/GuardClauses.cs @@ -45,4 +45,45 @@ public static void ThrowIfNegative(TimeSpan value) throw new ArgumentException("Time span cannot be negative."); } } + + /// + /// Throws an if the given value is negative. + /// + /// The value to validate. + /// The parameter name for the exception. + /// Thrown if is negative. + public static void ThrowIfNegative(long value, string name) + { + if (value < 0) + { + throw new ArgumentOutOfRangeException(name, value, "Value cannot be negative."); + } + } + + /// + /// Throws an if the given value is not positive. + /// + /// The value to validate. + /// The parameter name for the exception. + /// Thrown if is zero or negative. + public static void ThrowIfNotPositive(long value, string name) + { + if (value <= 0) + { + throw new ArgumentOutOfRangeException(name, value, "Value must be positive."); + } + } + + /// + /// Throws a if the stream trim mode is not supported. + /// + /// The stream trim mode to validate. + /// Thrown if is not . + public static void ThrowIfNotSupported(StreamTrimMode trimMode) + { + if (trimMode != StreamTrimMode.KeepReferences) + { + throw new NotImplementedException($"Stream trim mode {trimMode} is not supported by Valkey GLIDE"); + } + } } diff --git a/sources/Valkey.Glide/Internals/Helpers.cs b/sources/Valkey.Glide/Internals/Helpers.cs index 4cf012ab..17c856ac 100644 --- a/sources/Valkey.Glide/Internals/Helpers.cs +++ b/sources/Valkey.Glide/Internals/Helpers.cs @@ -10,11 +10,6 @@ internal static class Helpers public static Dictionary DownCastKeys(this Dictionary dict) => dict.Select(p => (Key: p.Key.ToString(), p.Value)).ToDictionary(p => p.Key, p => p.Value); - // TODO make recursive? - // Downcast dictionary values from `GlideString` to `string` - public static Dictionary DownCastVals(this Dictionary dict) where T : class - => dict.Select(p => (p.Key, Value: p.Value.ToString())).ToDictionary(p => p.Key, p => p.Value); - // Convert values in a dictionary from V to T where input dict has type `Dictionary` and returns `Dictionary` public static Dictionary ConvertValues(this Dictionary dict, Func converter) where K : notnull => dict.Select(p => (p.Key, Value: converter(p.Value))).ToDictionary(p => p.Key, p => p.Value); @@ -49,19 +44,20 @@ public static string GetRealTypeName(this Type t) } /// - /// Converts a KeyValuePair array to an array of key-value pairs for use with MSet command. + /// Skips the first elements from an async sequence. /// - /// The key-value pairs to convert. - /// An array where keys and values are interleaved: [key1, value1, key2, value2, ...] - public static GlideString[] ConvertKeyValuePairsToArray(KeyValuePair[] values) + public static async IAsyncEnumerable SkipAsync(this IAsyncEnumerable source, int skip) { - GlideString[] result = new GlideString[values.Length * 2]; - int index = 0; - foreach (KeyValuePair kvp in values) + int skipped = 0; + await foreach (T item in source) { - result[index++] = kvp.Key; - result[index++] = kvp.Value; + if (skipped < skip) + { + skipped++; + continue; + } + + yield return item; } - return result; } } diff --git a/sources/Valkey.Glide/Internals/Request.SetCommands.cs b/sources/Valkey.Glide/Internals/Request.SetCommands.cs index f62011c1..0c337bc6 100644 --- a/sources/Valkey.Glide/Internals/Request.SetCommands.cs +++ b/sources/Valkey.Glide/Internals/Request.SetCommands.cs @@ -70,23 +70,18 @@ public static Cmd SetRandomMemberAsync(ValkeyKey key) => new(RequestType.SRandMember, [key.ToGlideString()], true, response => response is null ? ValkeyValue.Null : (ValkeyValue)response, allowConverterToHandleNull: true); public static Cmd SetRandomMembersAsync(ValkeyKey key, long count) - => new(RequestType.SRandMember, [key.ToGlideString(), count.ToGlideString()], false, arr => [.. arr.Cast().Select(gs => (ValkeyValue)gs)]); + => new(RequestType.SRandMember, [key.ToGlideString(), count.ToGlideString()], false, ToValkeyValueArray); public static Cmd SetMoveAsync(ValkeyKey source, ValkeyKey destination, ValkeyValue value) => Simple(RequestType.SMove, [source.ToGlideString(), destination.ToGlideString(), value.ToGlideString()]); public static Cmd SetScanAsync(ValkeyKey key, long cursor, ScanOptions? options = null) - { - List args = [key.ToGlideString(), cursor.ToGlideString()]; - - args.AddRange(ToScanArgs(options)); + => new(RequestType.SScan, [key, cursor.ToGlideString(), .. options?.ToArgs() ?? []], false, ConvertSetScanResponse); - return new(RequestType.SScan, [.. args], false, arr => - { - object[] scanArray = arr; - long nextCursor = long.Parse(((GlideString)scanArray[0]).ToString()); - ValkeyValue[] elements = [.. ((object[])scanArray[1]).Cast().Select(gs => (ValkeyValue)gs)]; - return (nextCursor, elements); - }); + private static (long cursor, ValkeyValue[] items) ConvertSetScanResponse(object[] response) + { + var cursor = response[0] is long l ? l : long.Parse(response[0].ToString()!); + var items = ToValkeyValueArray((object[])response[1]); + return (cursor, items); } } diff --git a/sources/Valkey.Glide/Internals/Request.StreamCommands.cs b/sources/Valkey.Glide/Internals/Request.StreamCommands.cs index 99dc124c..e44706fd 100644 --- a/sources/Valkey.Glide/Internals/Request.StreamCommands.cs +++ b/sources/Valkey.Glide/Internals/Request.StreamCommands.cs @@ -184,41 +184,8 @@ public static Cmd StreamReadGroupAsync(StreamPosition pos public static Cmd StreamReadGroupAsync(IEnumerable positions, ValkeyValue group, ValkeyValue consumer, StreamReadGroupOptions options) => new(RequestType.XReadGroup, BuildStreamReadGroupArgs([.. positions], group, consumer, options), false, ConvertMultiStreamReadResponse, allowConverterToHandleNull: true); - // TODO SER only - public static Cmd StreamTrimAsync(ValkeyKey key, long? maxLength, ValkeyValue minId, bool useApproximateMaxLength, long? limit) - { - List args = [key]; - - if (maxLength.HasValue) - { - args.Add("MAXLEN"); - - if (useApproximateMaxLength) - { - args.Add("~"); - } - - args.Add(maxLength.Value.ToGlideString()); - } - else if (!minId.IsNull) - { - args.Add("MINID"); - if (useApproximateMaxLength) - { - args.Add("~"); - } - - args.Add(minId); - } - - if (limit.HasValue && useApproximateMaxLength) - { - args.Add("LIMIT"); - args.Add(limit.Value.ToGlideString()); - } - - return new(RequestType.XTrim, [.. args], false, response => response); - } + public static Cmd StreamTrimAsync(ValkeyKey key, StreamTrimOptions options) + => Simple(RequestType.XTrim, [key, .. options.ToArgs()]); #endregion diff --git a/sources/Valkey.Glide/Internals/Request.cs b/sources/Valkey.Glide/Internals/Request.cs index 990a66f3..4a1d7558 100644 --- a/sources/Valkey.Glide/Internals/Request.cs +++ b/sources/Valkey.Glide/Internals/Request.cs @@ -93,50 +93,8 @@ private static Dictionary ToValkeyKeyLongDict(Dictionary - /// Converts scan options to an array of arguments. - /// internal static GlideString[] ToScanArgs(ScanOptions? options) - { - if (options is null) - { - return []; - } - - List args = []; - - if (!options.MatchPattern.IsNull) - { - args.Add(ValkeyLiterals.MATCH); - args.Add(options.MatchPattern.ToGlideString()); - } - - if (options.Count.HasValue) - { - args.Add(ValkeyLiterals.COUNT); - args.Add(options.Count.Value.ToGlideString()); - } - - if (options.Type.HasValue) - { - args.Add(ValkeyLiterals.TYPE); - args.Add(ToType(options.Type.Value)); - } - - return [.. args]; - } - - private static GlideString ToType(ValkeyType type) => type switch - { - ValkeyType.String => "string", - ValkeyType.List => "list", - ValkeyType.Set => "set", - ValkeyType.SortedSet => "zset", - ValkeyType.Hash => "hash", - ValkeyType.Stream => "stream", - ValkeyType.Unknown or ValkeyType.None or _ => throw new ArgumentException($"Unsupported ValkeyType for SCAN: {type}") - }; + => options?.ToArgs() ?? []; /// /// Appends SetExpiryOptions arguments (PX/PXAT/KEEPTTL) to the args list. @@ -159,16 +117,22 @@ private static void AddExpiryArgs(List args, SetExpiryOptions optio } } - #region Set Converters + #region Collection Converters /// - /// Converts the given objects to an . + /// Converts the given objects to a set. /// private static ISet ToValkeyKeySet(IEnumerable items) => new HashSet(items.Cast().Select(gs => (ValkeyKey)gs.Bytes)); /// - /// Converts the given objects to an . + /// Converts the given objects to a array. + /// + private static ValkeyValue[] ToValkeyValueArray(object[] items) + => [.. items.Cast().Select(gs => (ValkeyValue)gs)]; + + /// + /// Converts the given objects to a set. /// private static ISet ToValkeyValueSet(IEnumerable items) => new HashSet(items.Cast().Select(gs => (ValkeyValue)gs)); diff --git a/sources/Valkey.Glide/Valkey.Glide.nuspec b/sources/Valkey.Glide/Valkey.Glide.nuspec index 90e3eb0a..166261f0 100644 --- a/sources/Valkey.Glide/Valkey.Glide.nuspec +++ b/sources/Valkey.Glide/Valkey.Glide.nuspec @@ -9,14 +9,12 @@ General Language Independent Driver for the Enterprise (GLIDE) for Valkey https://github.com/valkey-io/valkey-glide-csharp Apache-2.0 - README.md icon.png - + open-source performance database csharp key-value fault-tolerance cache reliability pubsub valkey valkey-client Copyright © 2025 Valkey GLIDE Maintainers - - + https://github.com/valkey-io/valkey-glide-csharp/releases @@ -29,8 +27,9 @@ - - + diff --git a/tests/Valkey.Glide.IntegrationTests/ConnectionManagementCommandTests.cs b/tests/Valkey.Glide.IntegrationTests/ConnectionManagementCommandTests.cs index 7158b574..232b7e1a 100644 --- a/tests/Valkey.Glide.IntegrationTests/ConnectionManagementCommandTests.cs +++ b/tests/Valkey.Glide.IntegrationTests/ConnectionManagementCommandTests.cs @@ -8,12 +8,16 @@ namespace Valkey.Glide.IntegrationTests; -[Collection("GlideTests")] -public class ConnectionManagementCommandTests(TestConfiguration config) +/// +/// Tests for connection management commands. +/// +[Collection(typeof(ConnectionManagementCommandTests))] +[CollectionDefinition(DisableParallelization = true)] +public class ConnectionManagementCommandTests(ServerFixture fixture) : IClassFixture { #region Constants - //TODO #414: Remove when ClientInfoAsync implemented. + // TODO #414: Remove when ClientInfoAsync implemented. private static readonly GlideString[] InfoCommand = ["CLIENT", "INFO"]; // Library version is set dynamically by the CD workflow, @@ -22,18 +26,15 @@ public class ConnectionManagementCommandTests(TestConfiguration config) Environment.GetEnvironmentVariable("GLIDE_VERSION") ?? "unknown"; #endregion - #region Public Properties - - public TestConfiguration Config { get; } = config; - - #endregion - #region Tests + #region ClientInfoAsync // TODO #414: Update when ClientInfoAsync implemented. - [Theory(DisableDiscoveryEnumeration = true)] - [MemberData(nameof(Config.TestClients), MemberType = typeof(TestConfiguration))] - public async Task TestClientInfo_ReportsCorrectLibNameAndVersion(BaseClient client) + [Theory] + [MemberData(nameof(Data.ClusterMode), MemberType = typeof(Data))] + public async Task TestClientInfo_ReportsCorrectLibNameAndVersion(bool clusterMode) { + await using var client = await fixture.GetServer(clusterMode).CreateClientAsync(); + var result = client is GlideClusterClient clusterClient ? (await clusterClient.CustomCommand(InfoCommand, Route.Random)).SingleValue : await ((GlideClient)client).CustomCommand(InfoCommand); @@ -45,18 +46,19 @@ public async Task TestClientInfo_ReportsCorrectLibNameAndVersion(BaseClient clie } // TODO #414: Update when ClientInfoAsync implemented. - [Theory(DisableDiscoveryEnumeration = true)] + [Theory] [MemberData(nameof(Data.ClusterMode), MemberType = typeof(Data))] - public async Task TestClientInfo_WithClientName_ReportsName(bool useCluster) + public async Task TestClientInfo_WithClientName_ReportsName(bool clusterMode) { const string clientName = "client"; - using BaseClient client = useCluster + + await using BaseClient client = clusterMode ? await GlideClusterClient.CreateClient( - TestConfiguration.DefaultClusterClientConfig() + fixture.ClusterServer.CreateConfigBuilder() .WithClientName(clientName) .Build()) : await GlideClient.CreateClient( - TestConfiguration.DefaultClientConfig() + fixture.StandaloneServer.CreateConfigBuilder() .WithClientName(clientName) .Build()); @@ -70,18 +72,21 @@ public async Task TestClientInfo_WithClientName_ReportsName(bool useCluster) #endregion #region ClientTrackingInfoAsync - [Theory(DisableDiscoveryEnumeration = true)] - [MemberData(nameof(TestConfiguration.TestClients), MemberType = typeof(TestConfiguration))] - public async Task ClientTrackingInfo_Off(BaseClient client) + [Theory] + [MemberData(nameof(Data.ClusterMode), MemberType = typeof(Data))] + public async Task ClientTrackingInfo_Off(bool clusterMode) { + await using var client = await fixture.GetServer(clusterMode).CreateClientAsync(); + var info = await client.ClientTrackingInfoAsync(); AssertTrackingInfoOff(info); } - [Theory(DisableDiscoveryEnumeration = true)] - [MemberData(nameof(TestConfiguration.TestClusterClients), MemberType = typeof(TestConfiguration))] - public async Task ClientTrackingInfo_Off_WithRoute(GlideClusterClient client) + [Fact] + public async Task ClientTrackingInfo_Off_WithRoute() { + await using GlideClusterClient client = await fixture.ClusterServer.CreateClusterClientAsync(); + var response = await client.ClientTrackingInfoAsync(Route.AllNodes); Assert.NotEmpty(response.MultiValue); @@ -99,11 +104,11 @@ public async Task ClientTrackingInfo_On(bool clusterMode) await using BaseClient client = clusterMode ? await GlideClusterClient.CreateClient( - TestConfiguration.DefaultClusterClientConfig() + fixture.ClusterServer.CreateConfigBuilder() .WithClientSideCache(cache) .Build()) : await GlideClient.CreateClient( - TestConfiguration.DefaultClientConfig() + fixture.StandaloneServer.CreateConfigBuilder() .WithClientSideCache(cache) .Build()); @@ -116,7 +121,7 @@ public async Task ClientTrackingInfo_On_WithRoute() var cache = BuildClientSideCacheConfig().WithServerAssisted(); await using var client = await GlideClusterClient.CreateClient( - TestConfiguration.DefaultClusterClientConfig() + fixture.ClusterServer.CreateConfigBuilder() .WithClientSideCache(cache) .Build()); @@ -129,10 +134,6 @@ public async Task ClientTrackingInfo_On_WithRoute() } } - /// - /// Asserts that the given - /// contains the expected values when tracking is turned off. - /// private static void AssertTrackingInfoOff(ClientTrackingInfo info) { Assert.Equivalent(new HashSet { "off" }, info.Flags); @@ -140,10 +141,6 @@ private static void AssertTrackingInfoOff(ClientTrackingInfo info) Assert.Empty(info.Prefixes); } - /// - /// Asserts that the given - /// contains the expected values when tracking is turned on. - /// private static void AssertTrackingInfoOn(ClientTrackingInfo info) { Assert.Equivalent(new HashSet { "on", "bcast" }, info.Flags); @@ -154,10 +151,12 @@ private static void AssertTrackingInfoOn(ClientTrackingInfo info) #endregion #region ClientPauseAsync / ClientUnpauseAsync - [Theory(DisableDiscoveryEnumeration = true)] - [MemberData(nameof(Config.TestClients), MemberType = typeof(TestConfiguration))] - public async Task TestClientPause_ReadsPausedUntilExpires(BaseClient client) + [Theory] + [MemberData(nameof(Data.ClusterMode), MemberType = typeof(Data))] + public async Task TestClientPause_ReadsPausedUntilExpires(bool clusterMode) { + await using var client = await fixture.GetServer(clusterMode).CreateClientAsync(); + var key = Guid.NewGuid().ToString(); await client.SetAsync(key, "value"); @@ -171,10 +170,12 @@ public async Task TestClientPause_ReadsPausedUntilExpires(BaseClient client) Assert.True(sw.Elapsed >= pauseFor); } - [Theory(DisableDiscoveryEnumeration = true)] - [MemberData(nameof(Config.TestClients), MemberType = typeof(TestConfiguration))] - public async Task TestClientPause_WritesPausedUntilExpires(BaseClient client) + [Theory] + [MemberData(nameof(Data.ClusterMode), MemberType = typeof(Data))] + public async Task TestClientPause_WritesPausedUntilExpires(bool clusterMode) { + await using var client = await fixture.GetServer(clusterMode).CreateClientAsync(); + var key = Guid.NewGuid().ToString(); await client.SetAsync(key, "before"); @@ -188,10 +189,12 @@ public async Task TestClientPause_WritesPausedUntilExpires(BaseClient client) Assert.True(sw.Elapsed >= pauseFor); } - [Theory(DisableDiscoveryEnumeration = true)] - [MemberData(nameof(Config.TestClients), MemberType = typeof(TestConfiguration))] - public async Task TestClientPauseWrite_ReadsNotPaused(BaseClient client) + [Theory] + [MemberData(nameof(Data.ClusterMode), MemberType = typeof(Data))] + public async Task TestClientPauseWrite_ReadsNotPaused(bool clusterMode) { + await using var client = await fixture.GetServer(clusterMode).CreateClientAsync(); + var key = Guid.NewGuid().ToString(); await client.SetAsync(key, "before"); @@ -207,10 +210,12 @@ public async Task TestClientPauseWrite_ReadsNotPaused(BaseClient client) await client.ClientUnpauseAsync(); } - [Theory(DisableDiscoveryEnumeration = true)] - [MemberData(nameof(Config.TestClients), MemberType = typeof(TestConfiguration))] - public async Task TestClientPauseWrite_ThenUnpause(BaseClient client) + [Theory] + [MemberData(nameof(Data.ClusterMode), MemberType = typeof(Data))] + public async Task TestClientPauseWrite_ThenUnpause(bool clusterMode) { + await using var client = await fixture.GetServer(clusterMode).CreateClientAsync(); + var key = Guid.NewGuid().ToString(); await client.SetAsync(key, "before"); @@ -232,16 +237,15 @@ public async Task TestClientPauseWrite_ThenUnpause(BaseClient client) [MemberData(nameof(Data.ClusterMode), MemberType = typeof(Data))] public async Task TestReset_ResetsConnectionState(bool clusterMode) { - // Create a client with server-assisted client-side caching. var cache = BuildClientSideCacheConfig().WithServerAssisted(); await using BaseClient client = clusterMode ? await GlideClusterClient.CreateClient( - TestConfiguration.DefaultClusterClientConfig() + fixture.ClusterServer.CreateConfigBuilder() .WithClientSideCache(cache) .Build()) : await GlideClient.CreateClient( - TestConfiguration.DefaultClientConfig() + fixture.StandaloneServer.CreateConfigBuilder() .WithClientSideCache(cache) .Build()); diff --git a/tests/Valkey.Glide.IntegrationTests/HashCommandTests.cs b/tests/Valkey.Glide.IntegrationTests/HashCommandTests.cs index e2f94351..b9f3b053 100644 --- a/tests/Valkey.Glide.IntegrationTests/HashCommandTests.cs +++ b/tests/Valkey.Glide.IntegrationTests/HashCommandTests.cs @@ -8,7 +8,6 @@ namespace Valkey.Glide.IntegrationTests; public class HashCommandTests(TestConfiguration config) { - // TODO #280: Cleanup this class public TestConfiguration Config { get; } = config; #region HashSetAsync diff --git a/tests/Valkey.Glide.IntegrationTests/SharedBatchTests.cs b/tests/Valkey.Glide.IntegrationTests/SharedBatchTests.cs index f3bf6191..99f8011a 100644 --- a/tests/Valkey.Glide.IntegrationTests/SharedBatchTests.cs +++ b/tests/Valkey.Glide.IntegrationTests/SharedBatchTests.cs @@ -9,8 +9,6 @@ namespace Valkey.Glide.IntegrationTests; -// TODO: even though collections aren't executed in parallel, tests in a collection still parallelized -// better to run tests in the named collections sequentially [Collection(typeof(SharedBatchTests))] [CollectionDefinition(DisableParallelization = true)] public class SharedBatchTests @@ -29,8 +27,12 @@ public async Task BatchRaiseOnError(BaseClient client, bool isAtomic) string key2 = "{BatchRaiseOnError}" + Guid.NewGuid(); Pipeline.IBatch batch = isCluster ? new ClusterBatch(isAtomic) : new Batch(isAtomic); - // TODO replace custom command - _ = batch.Set(key1, "hello").CustomCommand(["lpop", key1]).CustomCommand(["del", key1]).CustomCommand(["rename", key1, key2]); + + _ = batch + .Set(key1, "hello") + .ListLeftPop(key1) + .Delete(key1) + .Rename(key1, key2); object?[] res = isCluster ? (await ((GlideClusterClient)client).Exec((ClusterBatch)batch, false))! @@ -40,7 +42,7 @@ public async Task BatchRaiseOnError(BaseClient client, bool isAtomic) Assert.Multiple( () => Assert.Equal(4, res.Length), () => Assert.Equal(true, res[0]), - () => Assert.Equal(1L, (long)res[2]!), + () => Assert.Equal(true, res[2]), () => Assert.IsType(res[1]), () => Assert.IsType(res[3]), () => Assert.Contains("wrong kind of value", (res[1] as RequestException)!.Message), diff --git a/tests/Valkey.Glide.IntegrationTests/SharedCommandTests.cs b/tests/Valkey.Glide.IntegrationTests/SharedCommandTests.cs index 77c4cc28..40071b7a 100644 --- a/tests/Valkey.Glide.IntegrationTests/SharedCommandTests.cs +++ b/tests/Valkey.Glide.IntegrationTests/SharedCommandTests.cs @@ -38,7 +38,6 @@ internal async Task BatchTest(BatchTestData testData) } else { - // TODO use assertDeepEquals Assert.Equivalent(expectedInfo[i].ExpectedValue, actualResult[i]); } } diff --git a/tests/Valkey.Glide.IntegrationTests/Skip.cs b/tests/Valkey.Glide.IntegrationTests/Skip.cs index e1aa3fbe..4f7b9cb5 100644 --- a/tests/Valkey.Glide.IntegrationTests/Skip.cs +++ b/tests/Valkey.Glide.IntegrationTests/Skip.cs @@ -16,12 +16,12 @@ internal static class Skip private static readonly Version Valkey9_0 = new("9.0.0"); /// - /// Skips the test if hash field expiry commands are not supported. + /// Skips the test if background save cancel commands are not supported. /// - public static void IfHashExpireNotSupported() + public static void IfBgSaveCancelNotSupported() => Assert.SkipWhen( - TestConfiguration.SERVER_VERSION < Valkey9_0, - "Hash expire commands require Valkey 9.0+"); + TestConfiguration.SERVER_VERSION < Valkey8_1, + "Background save cancel commands require Valkey 8.1+"); /// /// Skips the test if bit index type commands are not supported. @@ -31,6 +31,14 @@ public static void IfBitIndexTypeNotSupported() TestConfiguration.SERVER_VERSION < Valkey7_0, "Bit index type commands require Valkey 7.0+"); + /// + /// Skips the test if hash field expiry commands are not supported. + /// + public static void IfHashExpireNotSupported() + => Assert.SkipWhen( + TestConfiguration.SERVER_VERSION < Valkey9_0, + "Hash expire commands require Valkey 9.0+"); + /// /// Skips the test if set intersection cardinality commands are not supported. /// @@ -40,12 +48,12 @@ public static void IfSetInterCardNotSupported() "Set intersection cardinality commands require Valkey 7.0+"); /// - /// Skips the test if background save cancel commands are not supported. + /// Skips the test if sorted set pop commands are not supported. /// - public static void IfBgSaveCancelNotSupported() + public static void IfSortedSetPopNotSupported() => Assert.SkipWhen( - TestConfiguration.SERVER_VERSION < Valkey8_1, - "Background save cancel commands require Valkey 8.1+"); + TestConfiguration.SERVER_VERSION < Valkey7_0, + "Sorted set pop commands (ZMPOP, BZMPOP, ZINTERCARD) require Valkey 7.0+"); #endregion #region Module Checks diff --git a/tests/Valkey.Glide.IntegrationTests/SortedSetCommandTests.cs b/tests/Valkey.Glide.IntegrationTests/SortedSetCommandTests.cs index 54cf5280..49440996 100644 --- a/tests/Valkey.Glide.IntegrationTests/SortedSetCommandTests.cs +++ b/tests/Valkey.Glide.IntegrationTests/SortedSetCommandTests.cs @@ -900,8 +900,8 @@ public async Task TestSortedSetIncrement(BaseClient client) [MemberData(nameof(Config.TestClients), MemberType = typeof(TestConfiguration))] public async Task TestSortedSetInterCard(BaseClient client) { - Assert.SkipWhen(TestConfiguration.IsVersionLessThan("7.0.0"), "ZINTERCARD is supported since 7.0.0" - ); + Skip.IfSortedSetPopNotSupported(); + string key1 = $"{{sortedSetKey}}-{Guid.NewGuid()}"; string key2 = $"{{sortedSetKey}}-{Guid.NewGuid()}"; string key3 = $"{{sortedSetKey}}-{Guid.NewGuid()}"; @@ -988,8 +988,8 @@ public async Task TestSortedSetLexCount(BaseClient client) [MemberData(nameof(Config.TestClients), MemberType = typeof(TestConfiguration))] public async Task TestSortedSetPopMultiKey(BaseClient client) { - Assert.SkipWhen(TestConfiguration.IsVersionLessThan("7.0.0"), "ZMPOP is supported since 7.0.0" - ); + Skip.IfSortedSetPopNotSupported(); + string key1 = $"{{sortedSetKey}}1-{Guid.NewGuid()}"; string key2 = $"{{sortedSetKey}}2-{Guid.NewGuid()}"; string emptyKey = $"{{sortedSetKey}}empty-{Guid.NewGuid()}"; @@ -1066,8 +1066,8 @@ public async Task TestSortedSetScores(BaseClient client) [MemberData(nameof(Config.TestClients), MemberType = typeof(TestConfiguration))] public async Task TestSortedSetBlockingPop(BaseClient client) { - Assert.SkipWhen(TestConfiguration.IsVersionLessThan("7.0.0"), "BZMPOP is supported since 7.0.0" - ); + Skip.IfSortedSetPopNotSupported(); + string key1 = $"{{testKey}}-{Guid.NewGuid()}"; string key2 = $"{{testKey}}-{Guid.NewGuid()}"; @@ -1119,9 +1119,8 @@ public async Task TestSortedSetBlockingPop(BaseClient client) [MemberData(nameof(Config.TestClients), MemberType = typeof(TestConfiguration))] public async Task TestSortedSetBlockingCommands_NonExistentKeys(BaseClient client) { - // TODO sorted sets - Assert.SkipWhen(TestConfiguration.IsVersionLessThan("7.0.0"), "BZMPOP is supported since 7.0.0" - ); + Skip.IfSortedSetPopNotSupported(); + string key1 = $"{{testKey}}-{Guid.NewGuid()}"; string key2 = $"{{testKey}}-{Guid.NewGuid()}"; @@ -1636,7 +1635,7 @@ public async Task TestSortedSetUnionAndStore(BaseClient client) [MemberData(nameof(Config.TestClients), MemberType = typeof(TestConfiguration))] public async Task TestSortedSetPopMinAsync_MultiKey(BaseClient client) { - Assert.SkipWhen(TestConfiguration.IsVersionLessThan("7.0.0"), "ZMPOP is supported since 7.0.0"); + Skip.IfSortedSetPopNotSupported(); string key1 = $"{{testKey}}-{Guid.NewGuid()}"; string key2 = $"{{testKey}}-{Guid.NewGuid()}"; @@ -1662,7 +1661,7 @@ public async Task TestSortedSetPopMinAsync_MultiKey(BaseClient client) [MemberData(nameof(Config.TestClients), MemberType = typeof(TestConfiguration))] public async Task TestSortedSetPopMaxAsync_MultiKey(BaseClient client) { - Assert.SkipWhen(TestConfiguration.IsVersionLessThan("7.0.0"), "ZMPOP is supported since 7.0.0"); + Skip.IfSortedSetPopNotSupported(); string key1 = $"{{testKey}}-{Guid.NewGuid()}"; @@ -1683,7 +1682,7 @@ public async Task TestSortedSetPopMaxAsync_MultiKey(BaseClient client) [MemberData(nameof(Config.TestClients), MemberType = typeof(TestConfiguration))] public async Task TestSortedSetPopMinAsync_MultiKey_WithCount(BaseClient client) { - Assert.SkipWhen(TestConfiguration.IsVersionLessThan("7.0.0"), "ZMPOP is supported since 7.0.0"); + Skip.IfSortedSetPopNotSupported(); string key1 = $"{{testKey}}-{Guid.NewGuid()}"; string key2 = $"{{testKey}}-{Guid.NewGuid()}"; @@ -1715,7 +1714,7 @@ public async Task TestSortedSetPopMinAsync_MultiKey_WithCount(BaseClient client) [MemberData(nameof(Config.TestClients), MemberType = typeof(TestConfiguration))] public async Task TestSortedSetPopMaxAsync_MultiKey_WithCount(BaseClient client) { - Assert.SkipWhen(TestConfiguration.IsVersionLessThan("7.0.0"), "ZMPOP is supported since 7.0.0"); + Skip.IfSortedSetPopNotSupported(); string key1 = $"{{testKey}}-{Guid.NewGuid()}"; diff --git a/tests/Valkey.Glide.IntegrationTests/StackExchange/SetCommandTests.cs b/tests/Valkey.Glide.IntegrationTests/StackExchange/SetCommandTests.cs index 352831d2..d886e288 100644 --- a/tests/Valkey.Glide.IntegrationTests/StackExchange/SetCommandTests.cs +++ b/tests/Valkey.Glide.IntegrationTests/StackExchange/SetCommandTests.cs @@ -85,20 +85,34 @@ public async Task SetScanAsync_WithPageSize_ReturnsAllMembers(IDatabaseAsync db) [MemberData(nameof(TestConfiguration.TestDatabases), MemberType = typeof(TestConfiguration))] public async Task SetScanAsync_WithPageOffset_SkipsElements(IDatabaseAsync db) { - string key = $"ser-sscan-offset-{Guid.NewGuid()}"; + var key = $"ser-sscan-offset-{Guid.NewGuid()}"; _ = await db.SetAddAsync(key, ["a", "b", "c", "d", "e"]); - // With pageOffset, we skip elements from the first page - List results = []; - await foreach (ValkeyValue value in db.SetScanAsync(key, pageSize: 1000, pageOffset: 2)) + var results = new List(); + await foreach (var value in db.SetScanAsync(key, pageOffset: 3)) { results.Add(value); } - // Should have 3 elements (5 - 2 skipped) - Assert.Equal(3, results.Count); + Assert.Equal(2, results.Count); + } + [Theory(DisableDiscoveryEnumeration = true)] + [MemberData(nameof(TestConfiguration.TestDatabases), MemberType = typeof(TestConfiguration))] + public async Task SetScanAsync_WithPageOffsetGreaterThanPageSize_SkipsElements(IDatabaseAsync db) + { + var key = $"ser-sscan-offset-gt-pagesize-{Guid.NewGuid()}"; + _ = await db.SetAddAsync(key, ["a", "b", "c", "d", "e"]); + + var results = new List(); + await foreach (var value in db.SetScanAsync(key, pageSize: 2, pageOffset: 3)) + { + results.Add(value); + } + + Assert.Equal(2, results.Count); } + #endregion #region SetAddAsync diff --git a/tests/Valkey.Glide.IntegrationTests/StackExchange/SortedSetCommandTests.cs b/tests/Valkey.Glide.IntegrationTests/StackExchange/SortedSetCommandTests.cs index 2add0771..fe29aaf8 100644 --- a/tests/Valkey.Glide.IntegrationTests/StackExchange/SortedSetCommandTests.cs +++ b/tests/Valkey.Glide.IntegrationTests/StackExchange/SortedSetCommandTests.cs @@ -932,24 +932,32 @@ public async Task SortedSetScanAsync_WithPageSize_ReturnsAllMembers(IDatabaseAsy [MemberData(nameof(TestConfiguration.TestDatabases), MemberType = typeof(TestConfiguration))] public async Task SortedSetScanAsync_WithPageOffset_SkipsElements(IDatabaseAsync db) { - string key = $"ser-zscan-offset-{Guid.NewGuid()}"; - _ = await db.SortedSetAddAsync(key, [ - new SortedSetEntry("a", 1.0), - new SortedSetEntry("b", 2.0), - new SortedSetEntry("c", 3.0), - new SortedSetEntry("d", 4.0), - new SortedSetEntry("e", 5.0) - ]); + var key = $"ser-zscan-offset-{Guid.NewGuid()}"; + _ = await db.SortedSetAddAsync(key, [new("a", 1.0), new("b", 2.0), new("c", 3.0), new("d", 4.0), new("e", 5.0)]); - // With pageOffset, we skip elements from the first page - List results = []; - await foreach (SortedSetEntry entry in db.SortedSetScanAsync(key, pageSize: 1000, pageOffset: 2)) + var results = new List(); + await foreach (var entry in db.SortedSetScanAsync(key, pageOffset: 3)) { results.Add(entry); } - // Should have 3 elements (5 - 2 skipped) - Assert.Equal(3, results.Count); + Assert.Equal(2, results.Count); + } + + [Theory(DisableDiscoveryEnumeration = true)] + [MemberData(nameof(TestConfiguration.TestDatabases), MemberType = typeof(TestConfiguration))] + public async Task SortedSetScanAsync_WithPageOffsetGreaterThanPageSize_SkipsElements(IDatabaseAsync db) + { + var key = $"ser-zscan-offset-gt-pagesize-{Guid.NewGuid()}"; + _ = await db.SortedSetAddAsync(key, [new("a", 1.0), new("b", 2.0), new("c", 3.0), new("d", 4.0), new("e", 5.0)]); + + var results = new List(); + await foreach (var entry in db.SortedSetScanAsync(key, pageSize: 2, pageOffset: 3)) + { + results.Add(entry); + } + + Assert.Equal(2, results.Count); } [Theory(DisableDiscoveryEnumeration = true)] diff --git a/tests/Valkey.Glide.IntegrationTests/StackExchange/ValkeyServerTests.cs b/tests/Valkey.Glide.IntegrationTests/StackExchange/ValkeyServerTests.cs index 7a06d51c..fd5cfd9d 100644 --- a/tests/Valkey.Glide.IntegrationTests/StackExchange/ValkeyServerTests.cs +++ b/tests/Valkey.Glide.IntegrationTests/StackExchange/ValkeyServerTests.cs @@ -22,64 +22,60 @@ public async Task KeysAsync_ReturnsMatchingKeys() var server = fixture.Server; var db = fixture.Database; - string prefix = Guid.NewGuid().ToString(); - string key1 = $"{prefix}:key1"; - string key2 = $"{prefix}:key2"; - string key3 = $"{prefix}:key3"; - string otherKey = "other:key"; - - _ = await db.StringSetAsync(key1, "value1"); - _ = await db.StringSetAsync(key2, "value2"); - _ = await db.StringSetAsync(key3, "value3"); - _ = await db.StringSetAsync(otherKey, "other"); - - List keys = []; - await foreach (ValkeyKey key in server.KeysAsync(pattern: $"{prefix}:*")) + var prefix = $"ser-keys-{Guid.NewGuid()}"; + foreach (var element in new[] { "a", "b", "c", "d", "e" }) { - keys.Add(key); + _ = await db.StringSetAsync($"{prefix}:{element}", "value"); } - Assert.Equal(3, keys.Count); - Assert.Contains(key1, keys.Select(k => k.ToString())); - Assert.Contains(key2, keys.Select(k => k.ToString())); - Assert.Contains(key3, keys.Select(k => k.ToString())); - Assert.DoesNotContain(otherKey, keys.Select(k => k.ToString())); + // Test scanning all keys + var results = new List(); + await foreach (var key in server.KeysAsync(pattern: $"{prefix}:*")) + { + results.Add(key); + } + + Assert.Equal(5, results.Count); + results.Clear(); // Test scanning with pageSize - keys.Clear(); - await foreach (ValkeyKey key in server.KeysAsync(pattern: $"{prefix}:*", pageSize: 1)) + await foreach (var key in server.KeysAsync(pattern: $"{prefix}:*", pageSize: 1)) { - keys.Add(key); + results.Add(key); } - Assert.Equal(3, keys.Count); + + Assert.Equal(5, results.Count); + results.Clear(); // Test scanning with pageOffset - keys.Clear(); - await foreach (ValkeyKey key in server.KeysAsync(pattern: $"{prefix}:*", pageOffset: 1)) + await foreach (var key in server.KeysAsync(pattern: $"{prefix}:*", pageOffset: 3)) { - keys.Add(key); + results.Add(key); } - Assert.True(keys.Count >= 2); + + Assert.Equal(2, results.Count); + results.Clear(); + + // Test scanning with pageOffset > pageSize + await foreach (var key in server.KeysAsync(pattern: $"{prefix}:*", pageSize: 2, pageOffset: 3)) + { + results.Add(key); + } + + Assert.Equal(2, results.Count); + results.Clear(); // Test scanning non-existent pattern - keys.Clear(); - await foreach (ValkeyKey key in server.KeysAsync(pattern: "nonexistent:*")) + await foreach (var key in server.KeysAsync(pattern: "nonexistent:*")) { - keys.Add(key); + results.Add(key); } - Assert.Empty(keys); - // Clear database. + Assert.Empty(results); + await server.FlushDatabaseAsync(); } - [Fact] - public async Task KeysAsync_CommandFlags_Throws() - => _ = await Assert.ThrowsAsync( - () => fixture.Server.KeysAsync(flags: UnsupportedCommandFlag) - .GetAsyncEnumerator(TestContext.Current.CancellationToken) - .MoveNextAsync().AsTask()); - [Fact] public async Task DatabaseSizeAsync_ReturnsSize() { diff --git a/tests/Valkey.Glide.IntegrationTests/StandaloneClientTests.cs b/tests/Valkey.Glide.IntegrationTests/StandaloneClientTests.cs index 5ccc9e14..f2268538 100644 --- a/tests/Valkey.Glide.IntegrationTests/StandaloneClientTests.cs +++ b/tests/Valkey.Glide.IntegrationTests/StandaloneClientTests.cs @@ -67,8 +67,6 @@ public async Task CanConnectWithDifferentParameters() [Theory(DisableDiscoveryEnumeration = true)] [MemberData(nameof(Config.TestStandaloneClients), MemberType = typeof(TestConfiguration))] - // Verify that client can handle complex return types, not just strings - // TODO: remove this test once we add tests with these commands public async Task CustomCommandWithDifferentReturnTypes(GlideClient client) { string key1 = Guid.NewGuid().ToString(); diff --git a/tests/Valkey.Glide.IntegrationTests/StreamCommandTests.cs b/tests/Valkey.Glide.IntegrationTests/StreamCommandTests.cs index abf7d202..63871607 100644 --- a/tests/Valkey.Glide.IntegrationTests/StreamCommandTests.cs +++ b/tests/Valkey.Glide.IntegrationTests/StreamCommandTests.cs @@ -817,7 +817,6 @@ public async Task StreamRangeAsync_DuplicateFieldNames(BaseClient client) Assert.Equal("value3", result[0].Values[2].Value.ToString()); } - // // TODO "legacy" // [Theory(DisableDiscoveryEnumeration = true)] // [MemberData(nameof(TestConfiguration.TestClients), MemberType = typeof(TestConfiguration))] // public async Task StreamCreateConsumerGroupAsync_LegacyOverload(BaseClient client) @@ -827,7 +826,6 @@ public async Task StreamRangeAsync_DuplicateFieldNames(BaseClient client) // _ = await client.StreamCreateConsumerGroupAsync(key, "mygroup", "0"); // } - // // TODO "legacy" // [Theory(DisableDiscoveryEnumeration = true)] // [MemberData(nameof(TestConfiguration.TestClients), MemberType = typeof(TestConfiguration))] // public async Task StreamReadGroupAsync_LegacyOverload_SingleStream(BaseClient client) @@ -839,7 +837,6 @@ public async Task StreamRangeAsync_DuplicateFieldNames(BaseClient client) // _ = Assert.Single(entries); // } - // // TODO "legacy" // [Theory(DisableDiscoveryEnumeration = true)] // [MemberData(nameof(TestConfiguration.TestClients), MemberType = typeof(TestConfiguration))] // public async Task StreamReadGroupAsync_LegacyOverload_MultipleStreams(BaseClient client) diff --git a/tests/Valkey.Glide.UnitTests/CommandTests.cs b/tests/Valkey.Glide.UnitTests/CommandTests.cs index 2847feee..b1d73b94 100644 --- a/tests/Valkey.Glide.UnitTests/CommandTests.cs +++ b/tests/Valkey.Glide.UnitTests/CommandTests.cs @@ -1006,9 +1006,9 @@ public void ValidateStreamCommandArgs() () => Assert.Equal(["XDEL", "key", "1-0", "2-0"], Request.StreamDeleteAsync("key", ["1-0", "2-0"]).GetArgs()), // StreamTrim - () => Assert.Equal(["XTRIM", "key", "MAXLEN", "1000"], Request.StreamTrimAsync("key", 1000, default, false, null).GetArgs()), - () => Assert.Equal(["XTRIM", "key", "MAXLEN", "~", "1000"], Request.StreamTrimAsync("key", 1000, default, true, null).GetArgs()), - () => Assert.Equal(["XTRIM", "key", "MINID", "0-1"], Request.StreamTrimAsync("key", null, "0-1", false, null).GetArgs()), + () => Assert.Equal(["XTRIM", "key", "MAXLEN", "1000"], Request.StreamTrimAsync("key", new StreamTrimOptions.MaxLen { MaxLength = 1000 }).GetArgs()), + () => Assert.Equal(["XTRIM", "key", "MAXLEN", "~", "1000"], Request.StreamTrimAsync("key", new StreamTrimOptions.MaxLen { MaxLength = 1000, Exact = false }).GetArgs()), + () => Assert.Equal(["XTRIM", "key", "MINID", "0-1"], Request.StreamTrimAsync("key", new StreamTrimOptions.MinId { MinEntryId = "0-1" }).GetArgs()), // StreamCreateConsumerGroup () => Assert.Equal(["XGROUPCREATE", "key", "group", "$", "MKSTREAM"], Request.StreamCreateConsumerGroupAsync("key", "group", default, true, null).GetArgs()), @@ -1095,7 +1095,7 @@ public void ValidateStreamCommandConverters() () => Assert.Equal(0L, Request.StreamDeleteAsync("key", ["1-0"]).Converter(0L)), // StreamTrim - () => Assert.Equal(10L, Request.StreamTrimAsync("key", 100, default, false, null).Converter(10L)), + () => Assert.Equal(10L, Request.StreamTrimAsync("key", new StreamTrimOptions.MaxLen { MaxLength = 100 }).Converter(10L)), // StreamCreateConsumerGroup () => Assert.True(Request.StreamCreateConsumerGroupAsync("key", "group", default, true, null).Converter("OK")),