Compare commits

...
80 Commits
Author SHA1 Message Date
Nils Andreas Svee b42fa20d2c test(irtt-stats): add memory growth benchmarks
Introduce tests with a custom global allocator to measure and report heap allocation trends across different statistics configurations. Note: temporarily commented out `#![forbid(unsafe_code)]` to support the counting allocator implementation.
2026-05-13 16:58:34 +07:00
Nils Andreas Svee ea85384ced docs(irtt-stats): clarify event contribution for rolling_count 2026-05-13 16:58:12 +07:00
Nils Andreas Svee aa6c3f4fa6 refactor(irtt-stats): remove unknown loss field 2026-05-13 16:25:11 +07:00
Nils Andreas Svee dd1b08c51c docs(irtt-stats): enforce public API documentation 2026-05-13 15:26:37 +07:00
Nils Andreas Svee a180388dc6 docs(irtt-stats): Add documentation comments to public-facing types and fields
Add docstrings to clarify the purpose of statistics collection types and describe the units and behaviors of metric fields.
2026-05-13 15:15:41 +07:00
Nils Andreas Svee 19e0e7aab9 refactor(irtt-stats): extract core aggregation module 2026-05-13 14:57:19 +07:00
Nils Andreas Svee c26008a0b6 refactor(irtt-stats): extract event normalization module 2026-05-13 14:53:13 +07:00
Nils Andreas Svee 1469a2d9e8 refactor(irtt-stats): extract rolling events module 2026-05-13 14:44:56 +07:00
Nils Andreas Svee ebc0d46621 refactor(irtt-stats): extract loss stats module 2026-05-13 14:32:12 +07:00
Nils Andreas Svee cc43b43f00 refactor(irtt-stats): extract IPDV tracking module 2026-05-13 14:25:54 +07:00
Nils Andreas Svee 04d5b634a2 refactor(irtt-stats): extract time stats module 2026-05-13 14:16:07 +07:00
Nils Andreas Svee 557c946796 refactor(irtt-stats): split snapshot builders 2026-05-13 14:07:43 +07:00
Nils Andreas Svee bda1d83c27 refactor(irtt-stats): extract rolling event storage 2026-05-13 13:59:54 +07:00
Nils Andreas Svee 820304f6eb refactor(irtt-stats): split unique reply accounting 2026-05-13 13:49:43 +07:00
Nils Andreas Svee f211845645 refactor(irtt-stats): extract IPDV update recording 2026-05-13 13:45:28 +07:00
Nils Andreas Svee 851825822d refactor(irtt-stats): split CoreStats event handlers 2026-05-13 13:40:55 +07:00
Nils Andreas Svee 969d26d0f1 refactor(irtt-stats): extract event normalization boundary 2026-05-13 13:31:27 +07:00
Nils Andreas Svee c1b2486046 refactor(irtt-stats): split reply and IPDV samples 2026-05-13 13:28:08 +07:00
Nils Andreas Svee 90bb29cd75 refactor(irtt-stats): extract IPDV tracker 2026-05-13 13:20:58 +07:00
Nils Andreas Svee 0dd137a59e refactor(irtt-stats): rename WindowEvent to StatsEvent 2026-05-13 13:12:06 +07:00
Nils Andreas Svee 05f9732760 refactor(irtt-stats): rename median mode to sample mode 2026-05-13 12:58:13 +07:00
Nils Andreas Svee ce4b3c37ab fix(irtt-stats): keep RTT IPDV independent from directional timing 2026-05-13 12:57:10 +07:00
Nils Andreas Svee f05b7cceb2 refactor(irtt-stats): improve duration safety by handling negative time offsets 2026-05-13 12:44:35 +07:00
Nils Andreas Svee 0a66f6fb49 refactor(irtt-stats): remove unused push_duration helper 2026-05-13 12:36:51 +07:00
Nils Andreas Svee 5138dee29f refactor(irtt-stats): unify duration statistics into a single TimeStats type
Simplify the statistics model by merging signed and unsigned duration types into a single structure that uses i128 to consistently handle both positive and negative time offsets.
2026-05-13 12:32:47 +07:00
Nils Andreas Svee 542e67bf74 refactor(irtt-stats): consolidate snapshot types and rename summary to snapshot 2026-05-13 12:21:51 +07:00
Nils Andreas Svee 197903fb8e test(irtt-stats): rename internal date-time helper and update test case name
Rename system_time_ns to unix_time_ns_after_epoch for better clarity on output behavior and update a continuous mode test case name to more accurately reflect its purpose.
2026-05-13 12:07:51 +07:00
Nils Andreas Svee adfe8e7a6b test(irtt-stats): move integration tests to dedicated test module 2026-05-13 11:59:06 +07:00
Nils Andreas Svee 72470bd196 test(client): update and expand DSCP negotiation test coverage 2026-05-13 00:37:54 +07:00
Nils Andreas Svee fec14b2c7c feat(session): restrict DSCP negotiation to allow only disabling (reset to 0) 2026-05-13 00:37:53 +07:00
Nils Andreas Svee 3dfca1ab5e refactor(session): implement Display and add documentation for NegotiationRestriction 2026-05-13 00:37:45 +07:00
Nils Andreas Svee cbe91f2fe0 feat(client): track and report specific negotiation restrictions
Replace basic validation with a detailed negotiation process that records specific parameter changes. This allows the client to provide better feedback when a server restricts requested session parameters like duration or DSCP.
2026-05-12 15:18:02 +07:00
Nils Andreas Svee a7e10358fd test(client): use fallible packet length helpers in integration tests 2026-05-12 13:05:45 +07:00
Nils Andreas Svee 4549d95d0a test(client): update test helpers to handle fallible packet length calculations 2026-05-12 13:05:45 +07:00
Nils Andreas Svee 1b79c864d4 refactor(client): propagate ProtoError from recv_buffer_size calculation 2026-05-12 13:05:45 +07:00
Nils Andreas Svee 20a1d9dc8e refactor(proto): make echo_packet_len return Result to handle negative lengths 2026-05-12 13:05:45 +07:00
Nils Andreas Svee b508a94198 build: bump version to 0.2.0 2026-05-11 15:11:58 +07:00
Nils Andreas Svee 4ed09e2209 ci: add cargo-dist for automated release management
Introduce cargo-dist workflows and configuration to automate binary building and GitHub Release creation across multiple platforms.
2026-05-11 15:10:15 +07:00
Nils Andreas Svee e2a9d68843 ci: add actions read permission to OSV-Scanner workflow
Grant necessary read access to actions to ensure the vulnerability scanner can evaluate the execution environment.
2026-05-11 15:01:05 +07:00
renovate[bot] c04e8f7084 chore(deps): maintain cargo lockfile 2026-05-11 08:00:31 +00:00
renovate[bot] 323e5f7f3d chore(deps): update github actions 2026-05-11 14:58:04 +07:00
Nils Andreas Svee 7ad83ca1f6 refactor: remove logical_seq from client events, session tracking and stats usage
It was an attempt at handling integer wraparound more cleanly keeping in mind that we have a continuous mode,
and so we can more easily reach seq > u32::MAX

However after running the numbers, if we run with a interval of 10ms, it's still gonna take ~497 days before we hit integer overflow, so for now it doesn't seem worth to have this extra logic to handle something that'll in 99.9% of cases be purely an edge case

Keeping in mind autorate usage, in sqm-autorate-rust each send loop is 500 ms by default, and the sleep is then 500 ms / 5 reflectors, so our interval is 100 ms. that means it'll take 4970 days or ~13.6 years to reach the limit. Not worth dealing with IMO
2026-05-11 14:56:07 +07:00
Nils Andreas Svee 8ef53c999a build: optimize release profile for binary size and performance
Enable LTO, single codegen units, and symbol stripping to reduce binary footprint and improve execution efficiency.
2026-05-11 13:27:50 +07:00
Nils Andreas Svee 21bc7805f7 refactor: mark public enums as non-exhaustive to improve API stability
Apply the #[non-exhaustive] attribute to ClientError and WarningKind to prevent breaking changes when adding new variants. Handle the catch-all case in the CLI formatter.
2026-05-11 12:33:40 +07:00
Nils Andreas Svee 8d1d0f50de style(client): reformat imports in test files
Collapse multi-line import statements into single lines to improve code legibility and maintain consistent formatting across test suites.
2026-05-11 10:13:06 +07:00
Nils Andreas Svee d6c6e60742 refactor(client): expose len methods for map structures 2026-05-11 10:10:24 +07:00
Nils Andreas Svee 1cdd100473 refactor(irtt-stats): remove redundant process_with_update method
Simplify the StatsCollector API by removing an unnecessary indirection that performed the same logic as the main process method.
2026-05-11 10:09:13 +07:00
Nils Andreas Svee 80fda92231 fix(irtt-stats): use event timestamp for warnings instead of current time
Ensure warning statistics reflect the actual time the event occurred rather than the moment the event was processed.
2026-05-11 09:55:11 +07:00
Nils Andreas Svee 5f6bff5dcb feat(client): add timestamp to ClientEvent::Warning
Include a timestamp when reporting warnings to allow consumers like the machine-readable formatter to provide temporal context for troubleshooting.
2026-05-11 09:50:16 +07:00
Nils Andreas Svee 1a04f44bb5 refactor(client): remove redundant timestamp parameter from poll_timeouts
Use internal monotonic time by default and introduce poll_timeouts_at for callers requiring a specific sampled Instant. This simplifies the API for standard usage while maintaining flexibility for external event loops.
2026-05-11 09:33:55 +07:00
Nils Andreas Svee d8b580a19c refactor(client): remove redundant timestamp parameter from close method 2026-05-10 23:47:46 +07:00
Nils Andreas Svee 023e85741a refactor(client): remove redundant timestamp parameter from open method 2026-05-10 23:37:35 +07:00
Nils Andreas Svee 2258509021 chore: bump version to 0.1.1 across all crates
Update workspace members and dependencies to version 0.1.1 to prepare for a new release.
2026-05-10 21:17:22 +07:00
renovate[bot] e6a296baf4 chore(deps): pin dependencies 2026-05-10 20:56:43 +07:00
renovate[bot] 14be5cd836 chore(deps): update github actions to v2 2026-05-10 20:56:43 +07:00
Nils Andreas Svee 6b373380f4 ci: pin GitHub Action digests in Renovate configuration
Add the pinGitHubActionDigests helper to improve CI security by using immutable commit SHA digests instead of mutable tags.
2026-05-10 20:56:43 +07:00
Nils Andreas Svee fc8e6d10e8 ci: add cargo-semver-checks to verify API compatibility
Check for breaking changes against all features on tags and manual triggers to ensure semantic versioning compliance.
2026-05-10 20:56:43 +07:00
Nils Andreas Svee 1a314778ad ci: expand workflow security auditing with actionlint
Rename the zizmor workflow to actions-security and add actionlint to provide comprehensive static analysis and linting for GitHub Actions.
2026-05-10 20:56:43 +07:00
Nils Andreas Svee 2e9ad57d61 ci: remove redundant build and test workflow
Deletes the basic build_and_test.yml to avoid duplication with more specialized CI pipelines and security scanners.
2026-05-10 20:56:43 +07:00
Nils Andreas Svee b00d3ef633 ci: add zizmor for GitHub Actions security scanning 2026-05-10 20:56:43 +07:00
Nils Andreas Svee ae5f939fb2 ci: implement supply chain security and workflow linting 2026-05-10 20:56:43 +07:00
Nils Andreas Svee 385d531461 chore: refine cargo-deny policy for stricter dependency auditing
Replace the default template with a tailored configuration that enforces explicit license checks, denies unknown registries, and mandates exact commit pins for git dependencies.
2026-05-10 20:56:43 +07:00
Nils Andreas Svee 00aab3dd6b ci: modernize clippy sarif workflow 2026-05-10 20:56:43 +07:00
Nils Andreas Svee 8343bcbb54 ci: add primary rust continuous integration pipeline 2026-05-10 20:56:43 +07:00
Nils Andreas Svee b8d60fcc23 chore: add cargo-deny configuration for dependency auditing 2026-05-10 20:56:43 +07:00
Nils Andreas Svee 44ffb9557a fix(client): use correct socket option for IPv4 TOS reception on Linux 2026-05-10 20:56:43 +07:00
Nils Andreas Svee 26fbe86e90 chore: add OSV-Scanner workflow for vulnerability scanning 2026-05-10 20:56:43 +07:00
Nils Andreas Svee b3f4a2d9eb chore: add rust-clippy workflow for code analysis 2026-05-10 20:56:43 +07:00
renovate[bot] d4225f38fe chore(deps): update github actions to v6 2026-05-10 20:56:43 +07:00
Nils Andreas Svee c386f87a0e fix(deps): update rust crate ctrlc to 3.5.2 2026-05-10 20:56:43 +07:00
renovate[bot] 80255f1fff fix(deps): update rust crate clap to 4.6.1 2026-05-10 20:56:43 +07:00
renovate[bot] 3eaaeee408 fix(deps): update socket dependencies 2026-05-10 20:56:43 +07:00
renovate[bot] f474632f4c fix(deps): update rust crate thiserror to 2.0.18 2026-05-10 20:56:43 +07:00
renovate[bot] ababa9d6b6 fix(deps): update rust crate subtle to 2.6.1 2026-05-10 20:56:43 +07:00
Nils Andreas Svee 47a8143bce refactor(irtt-proto): update HMAC implementation for Digest 0.11 2026-05-10 20:56:43 +07:00
Nils Andreas Svee 831fd0cc45 build(deps): update hmac and md-5 dependencies 2026-05-10 20:56:43 +07:00
Nils Andreas Svee fe418d650d refactor(client): upgrade socket2 to 0.6 and use version-specific API 2026-05-10 20:56:43 +07:00
Nils Andreas Svee 13404297c4 build: update windows-sys dependencies and consolidate versions 2026-05-10 20:56:43 +07:00
Nils Andreas Svee 8d24d4811a chore: upgrade ctrlc to version 3.5 2026-05-10 20:56:43 +07:00
renovate[bot] 7e17f063ea chore: configure renovate 2026-05-10 20:56:43 +07:00
59 changed files with 3647 additions and 2139 deletions

No files matched your search

+52
View File
@@ -0,0 +1,52 @@
name: GitHub Actions Security
on:
push:
branches:
- main
pull_request:
branches:
- main
merge_group:
schedule:
- cron: "11 3 * * 1"
workflow_dispatch:
permissions:
contents: read
jobs:
actionlint:
name: actionlint
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
persist-credentials: false
- name: Run actionlint
uses: raven-actions/actionlint@205b530c5d9fa8f44ae9ed59f341a0db994aa6f8 # v2
zizmor:
name: zizmor
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: read
actions: read
security-events: write
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
persist-credentials: false
- name: Run zizmor
uses: zizmorcore/zizmor-action@b1d7e1fb5de872772f31590499237e7cce841e8e # v0.5.3
with:
persona: regular
-21
View File
@@ -1,21 +0,0 @@
name: Build and Test
on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]
env:
CARGO_TERM_COLOR: always
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build
run: cargo build --verbose
- name: Run tests
run: cargo test --verbose
+115
View File
@@ -0,0 +1,115 @@
name: Rust CI
on:
push:
branches:
- main
pull_request:
branches:
- main
merge_group:
workflow_dispatch:
permissions:
contents: read
env:
CARGO_TERM_COLOR: always
jobs:
fmt:
name: cargo fmt
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
persist-credentials: false
- name: Install Rust toolchain
uses: actions-rust-lang/setup-rust-toolchain@46268bd060767258de96ed93c1251119784f2ab6 # v1
with:
components: rustfmt
cache-key: fmt
- name: Check formatting
uses: actions-rust-lang/rustfmt@4066006ec54a31931b9b1fddfd38f2fdf2d27143 # v1
clippy:
name: cargo clippy
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
persist-credentials: false
- name: Install Rust toolchain
uses: actions-rust-lang/setup-rust-toolchain@46268bd060767258de96ed93c1251119784f2ab6 # v1
with:
components: clippy
cache-key: clippy
- name: Run clippy
run: |
cargo clippy \
--workspace \
--all-targets \
--all-features \
--locked \
-- -D warnings
test:
name: cargo test
runs-on: ubuntu-latest
timeout-minutes: 25
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
persist-credentials: false
- name: Install Rust toolchain
uses: actions-rust-lang/setup-rust-toolchain@46268bd060767258de96ed93c1251119784f2ab6 # v1
with:
cache-key: test
- name: Run tests
run: |
cargo test \
--workspace \
--all-targets \
--all-features \
--locked
docs:
name: cargo doc
runs-on: ubuntu-latest
timeout-minutes: 20
env:
RUSTDOCFLAGS: "-D warnings"
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
persist-credentials: false
- name: Install Rust toolchain
uses: actions-rust-lang/setup-rust-toolchain@46268bd060767258de96ed93c1251119784f2ab6 # v1
with:
cache-key: docs
- name: Check docs
run: |
cargo doc \
--workspace \
--all-features \
--no-deps \
--locked
+49
View File
@@ -0,0 +1,49 @@
# This workflow uses actions that are not certified by GitHub.
# They are provided by a third-party and are governed by
# separate terms of service, privacy policy, and support
# documentation.
# A sample workflow which sets up periodic OSV-Scanner scanning for vulnerabilities,
# in addition to a PR check which fails if new vulnerabilities are introduced.
#
# For more examples and options, including how to ignore specific vulnerabilities,
# see https://google.github.io/osv-scanner/github-action/
name: OSV-Scanner
on:
pull_request:
branches: ["main"]
merge_group:
branches: ["main"]
schedule:
- cron: "45 8 * * 1"
push:
branches: ["main"]
permissions:
# Require writing security events to upload SARIF file to security tab
security-events: write
# Read commit contents
contents: read
actions: read
jobs:
scan-scheduled:
if: ${{ github.event_name == 'push' || github.event_name == 'schedule' }}
uses: "google/osv-scanner-action/.github/workflows/osv-scanner-reusable.yml@9a498708959aeaef5ef730655706c5a1df1edbc2" # v2.3.8
with:
# Example of specifying custom arguments
scan-args: |-
-r
--skip-git
./
scan-pr:
if: ${{ github.event_name == 'pull_request' || github.event_name == 'merge_group' }}
uses: "google/osv-scanner-action/.github/workflows/osv-scanner-reusable-pr.yml@9a498708959aeaef5ef730655706c5a1df1edbc2" # v2.3.8
with:
# Example of specifying custom arguments
scan-args: |-
-r
--skip-git
./
+296
View File
@@ -0,0 +1,296 @@
# This file was autogenerated by dist: https://axodotdev.github.io/cargo-dist
#
# Copyright 2022-2024, axodotdev
# SPDX-License-Identifier: MIT or Apache-2.0
#
# CI that:
#
# * checks for a Git Tag that looks like a release
# * builds artifacts with dist (archives, installers, hashes)
# * uploads those artifacts to temporary workflow zip
# * on success, uploads the artifacts to a GitHub Release
#
# Note that the GitHub Release will be created with a generated
# title/body based on your changelogs.
name: Release
permissions:
"contents": "write"
# This task will run whenever you push a git tag that looks like a version
# like "1.0.0", "v0.1.0-prerelease.1", "my-app/0.1.0", "releases/v1.0.0", etc.
# Various formats will be parsed into a VERSION and an optional PACKAGE_NAME, where
# PACKAGE_NAME must be the name of a Cargo package in your workspace, and VERSION
# must be a Cargo-style SemVer Version (must have at least major.minor.patch).
#
# If PACKAGE_NAME is specified, then the announcement will be for that
# package (erroring out if it doesn't have the given version or isn't dist-able).
#
# If PACKAGE_NAME isn't specified, then the announcement will be for all
# (dist-able) packages in the workspace with that version (this mode is
# intended for workspaces with only one dist-able package, or with all dist-able
# packages versioned/released in lockstep).
#
# If you push multiple tags at once, separate instances of this workflow will
# spin up, creating an independent announcement for each one. However, GitHub
# will hard limit this to 3 tags per commit, as it will assume more tags is a
# mistake.
#
# If there's a prerelease-style suffix to the version, then the release(s)
# will be marked as a prerelease.
on:
pull_request:
push:
tags:
- '**[0-9]+.[0-9]+.[0-9]+*'
jobs:
# Run 'dist plan' (or host) to determine what tasks we need to do
plan:
runs-on: "ubuntu-22.04"
outputs:
val: ${{ steps.plan.outputs.manifest }}
tag: ${{ !github.event.pull_request && github.ref_name || '' }}
tag-flag: ${{ !github.event.pull_request && format('--tag={0}', github.ref_name) || '' }}
publishing: ${{ !github.event.pull_request }}
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
steps:
- uses: actions/checkout@v6
with:
persist-credentials: false
submodules: recursive
- name: Install dist
# we specify bash to get pipefail; it guards against the `curl` command
# failing. otherwise `sh` won't catch that `curl` returned non-0
shell: bash
run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.31.0/cargo-dist-installer.sh | sh"
- name: Cache dist
uses: actions/upload-artifact@v6
with:
name: cargo-dist-cache
path: ~/.cargo/bin/dist
# sure would be cool if github gave us proper conditionals...
# so here's a doubly-nested ternary-via-truthiness to try to provide the best possible
# functionality based on whether this is a pull_request, and whether it's from a fork.
# (PRs run on the *source* but secrets are usually on the *target* -- that's *good*
# but also really annoying to build CI around when it needs secrets to work right.)
- id: plan
run: |
dist ${{ (!github.event.pull_request && format('host --steps=create --tag={0}', github.ref_name)) || 'plan' }} --output-format=json > plan-dist-manifest.json
echo "dist ran successfully"
cat plan-dist-manifest.json
echo "manifest=$(jq -c "." plan-dist-manifest.json)" >> "$GITHUB_OUTPUT"
- name: "Upload dist-manifest.json"
uses: actions/upload-artifact@v6
with:
name: artifacts-plan-dist-manifest
path: plan-dist-manifest.json
# Build and packages all the platform-specific things
build-local-artifacts:
name: build-local-artifacts (${{ join(matrix.targets, ', ') }})
# Let the initial task tell us to not run (currently very blunt)
needs:
- plan
if: ${{ fromJson(needs.plan.outputs.val).ci.github.artifacts_matrix.include != null && (needs.plan.outputs.publishing == 'true' || fromJson(needs.plan.outputs.val).ci.github.pr_run_mode == 'upload') }}
strategy:
fail-fast: false
# Target platforms/runners are computed by dist in create-release.
# Each member of the matrix has the following arguments:
#
# - runner: the github runner
# - dist-args: cli flags to pass to dist
# - install-dist: expression to run to install dist on the runner
#
# Typically there will be:
# - 1 "global" task that builds universal installers
# - N "local" tasks that build each platform's binaries and platform-specific installers
matrix: ${{ fromJson(needs.plan.outputs.val).ci.github.artifacts_matrix }}
runs-on: ${{ matrix.runner }}
container: ${{ matrix.container && matrix.container.image || null }}
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
BUILD_MANIFEST_NAME: target/distrib/${{ join(matrix.targets, '-') }}-dist-manifest.json
steps:
- name: enable windows longpaths
run: |
git config --global core.longpaths true
- uses: actions/checkout@v6
with:
persist-credentials: false
submodules: recursive
- name: Install Rust non-interactively if not already installed
if: ${{ matrix.container }}
run: |
if ! command -v cargo > /dev/null 2>&1; then
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
echo "$HOME/.cargo/bin" >> $GITHUB_PATH
fi
- name: Install dist
run: ${{ matrix.install_dist.run }}
# Get the dist-manifest
- name: Fetch local artifacts
uses: actions/download-artifact@v7
with:
pattern: artifacts-*
path: target/distrib/
merge-multiple: true
- name: Install dependencies
run: |
${{ matrix.packages_install }}
- name: Build artifacts
run: |
# Actually do builds and make zips and whatnot
dist build ${{ needs.plan.outputs.tag-flag }} --print=linkage --output-format=json ${{ matrix.dist_args }} > dist-manifest.json
echo "dist ran successfully"
- id: cargo-dist
name: Post-build
# We force bash here just because github makes it really hard to get values up
# to "real" actions without writing to env-vars, and writing to env-vars has
# inconsistent syntax between shell and powershell.
shell: bash
run: |
# Parse out what we just built and upload it to scratch storage
echo "paths<<EOF" >> "$GITHUB_OUTPUT"
dist print-upload-files-from-manifest --manifest dist-manifest.json >> "$GITHUB_OUTPUT"
echo "EOF" >> "$GITHUB_OUTPUT"
cp dist-manifest.json "$BUILD_MANIFEST_NAME"
- name: "Upload artifacts"
uses: actions/upload-artifact@v6
with:
name: artifacts-build-local-${{ join(matrix.targets, '_') }}
path: |
${{ steps.cargo-dist.outputs.paths }}
${{ env.BUILD_MANIFEST_NAME }}
# Build and package all the platform-agnostic(ish) things
build-global-artifacts:
needs:
- plan
- build-local-artifacts
runs-on: "ubuntu-22.04"
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
BUILD_MANIFEST_NAME: target/distrib/global-dist-manifest.json
steps:
- uses: actions/checkout@v6
with:
persist-credentials: false
submodules: recursive
- name: Install cached dist
uses: actions/download-artifact@v7
with:
name: cargo-dist-cache
path: ~/.cargo/bin/
- run: chmod +x ~/.cargo/bin/dist
# Get all the local artifacts for the global tasks to use (for e.g. checksums)
- name: Fetch local artifacts
uses: actions/download-artifact@v7
with:
pattern: artifacts-*
path: target/distrib/
merge-multiple: true
- id: cargo-dist
shell: bash
run: |
dist build ${{ needs.plan.outputs.tag-flag }} --output-format=json "--artifacts=global" > dist-manifest.json
echo "dist ran successfully"
# Parse out what we just built and upload it to scratch storage
echo "paths<<EOF" >> "$GITHUB_OUTPUT"
jq --raw-output ".upload_files[]" dist-manifest.json >> "$GITHUB_OUTPUT"
echo "EOF" >> "$GITHUB_OUTPUT"
cp dist-manifest.json "$BUILD_MANIFEST_NAME"
- name: "Upload artifacts"
uses: actions/upload-artifact@v6
with:
name: artifacts-build-global
path: |
${{ steps.cargo-dist.outputs.paths }}
${{ env.BUILD_MANIFEST_NAME }}
# Determines if we should publish/announce
host:
needs:
- plan
- build-local-artifacts
- build-global-artifacts
# Only run if we're "publishing", and only if plan, local and global didn't fail (skipped is fine)
if: ${{ always() && needs.plan.result == 'success' && needs.plan.outputs.publishing == 'true' && (needs.build-global-artifacts.result == 'skipped' || needs.build-global-artifacts.result == 'success') && (needs.build-local-artifacts.result == 'skipped' || needs.build-local-artifacts.result == 'success') }}
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
runs-on: "ubuntu-22.04"
outputs:
val: ${{ steps.host.outputs.manifest }}
steps:
- uses: actions/checkout@v6
with:
persist-credentials: false
submodules: recursive
- name: Install cached dist
uses: actions/download-artifact@v7
with:
name: cargo-dist-cache
path: ~/.cargo/bin/
- run: chmod +x ~/.cargo/bin/dist
# Fetch artifacts from scratch-storage
- name: Fetch artifacts
uses: actions/download-artifact@v7
with:
pattern: artifacts-*
path: target/distrib/
merge-multiple: true
- id: host
shell: bash
run: |
dist host ${{ needs.plan.outputs.tag-flag }} --steps=upload --steps=release --output-format=json > dist-manifest.json
echo "artifacts uploaded and released successfully"
cat dist-manifest.json
echo "manifest=$(jq -c "." dist-manifest.json)" >> "$GITHUB_OUTPUT"
- name: "Upload dist-manifest.json"
uses: actions/upload-artifact@v6
with:
# Overwrite the previous copy
name: artifacts-dist-manifest
path: dist-manifest.json
# Create a GitHub Release while uploading all files to it
- name: "Download GitHub Artifacts"
uses: actions/download-artifact@v7
with:
pattern: artifacts-*
path: artifacts
merge-multiple: true
- name: Cleanup
run: |
# Remove the granular manifests
rm -f artifacts/*-dist-manifest.json
- name: Create GitHub Release
env:
PRERELEASE_FLAG: "${{ fromJson(steps.host.outputs.manifest).announcement_is_prerelease && '--prerelease' || '' }}"
ANNOUNCEMENT_TITLE: "${{ fromJson(steps.host.outputs.manifest).announcement_title }}"
ANNOUNCEMENT_BODY: "${{ fromJson(steps.host.outputs.manifest).announcement_github_body }}"
RELEASE_COMMIT: "${{ github.sha }}"
run: |
# Write and read notes from a file to avoid quoting breaking things
echo "$ANNOUNCEMENT_BODY" > $RUNNER_TEMP/notes.txt
gh release create "${{ needs.plan.outputs.tag }}" --target "$RELEASE_COMMIT" $PRERELEASE_FLAG --title "$ANNOUNCEMENT_TITLE" --notes-file "$RUNNER_TEMP/notes.txt" artifacts/*
announce:
needs:
- plan
- host
# use "always() && ..." to allow us to wait for all publish jobs while
# still allowing individual publish jobs to skip themselves (for prereleases).
# "host" however must run to completion, no skipping allowed!
if: ${{ always() && needs.host.result == 'success' }}
runs-on: "ubuntu-22.04"
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
steps:
- uses: actions/checkout@v6
with:
persist-credentials: false
submodules: recursive
+72
View File
@@ -0,0 +1,72 @@
name: Clippy SARIF
on:
push:
branches:
- main
schedule:
- cron: "17 12 * * 4"
workflow_dispatch:
permissions:
contents: read
security-events: write
actions: read
env:
CARGO_TERM_COLOR: always
jobs:
clippy-sarif:
name: Clippy SARIF
runs-on: ubuntu-latest
timeout-minutes: 25
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
persist-credentials: false
- name: Install Rust toolchain
uses: actions-rust-lang/setup-rust-toolchain@46268bd060767258de96ed93c1251119784f2ab6 # v1
with:
components: clippy
cache-key: clippy-sarif
- name: Install SARIF tools
run: |
cargo install --locked clippy-sarif sarif-fmt
- name: Run Clippy and generate SARIF
id: clippy
shell: bash
run: |
set +e
cargo clippy \
--workspace \
--all-targets \
--all-features \
--locked \
--message-format=json \
| clippy-sarif \
| tee rust-clippy-results.sarif \
| sarif-fmt
clippy_status=${PIPESTATUS[0]}
echo "exit_code=${clippy_status}" >> "$GITHUB_OUTPUT"
exit 0
- name: Upload Clippy SARIF
uses: github/codeql-action/upload-sarif@68bde559dea0fdcac2102bfdf6230c5f70eb485e # v4
if: always()
with:
sarif_file: rust-clippy-results.sarif
category: clippy
wait-for-processing: true
- name: Fail if Clippy failed
if: steps.clippy.outputs.exit_code != '0'
run: exit 1
+27
View File
@@ -0,0 +1,27 @@
name: Semver
on:
workflow_dispatch:
push:
tags:
- "v*.*.*"
permissions:
contents: read
jobs:
semver:
name: cargo-semver-checks
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
persist-credentials: false
- name: Check semver compatibility
uses: obi1kenobi/cargo-semver-checks-action@6b69fcf40e9b5fb17adeb57e4b6ecd020649a239 # v2
with:
feature-group: all-features
+64
View File
@@ -0,0 +1,64 @@
name: Supply chain
on:
push:
branches:
- main
pull_request:
branches:
- main
merge_group:
schedule:
- cron: "41 2 * * 1"
workflow_dispatch:
permissions:
contents: read
jobs:
cargo-deny:
name: cargo-deny ${{ matrix.checks }}
runs-on: ubuntu-latest
timeout-minutes: 15
strategy:
fail-fast: false
matrix:
include:
# Advisories can appear after code was merged. Keep this visible,
# but don't let a surprise upstream advisory nuke every unrelated PR.
- checks: advisories
continue-on-error: true
# These are policy choices in this repo. They should block PRs.
- checks: bans licenses sources
continue-on-error: false
continue-on-error: ${{ matrix.continue-on-error }}
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
persist-credentials: false
- name: Run cargo-deny
uses: EmbarkStudios/cargo-deny-action@6c8f9facfa5047ec02d8485b6bf52b587b7777d1 # v2
with:
command: check
arguments: --all-features
command-arguments: ${{ matrix.checks }}
actionlint:
name: actionlint
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
persist-credentials: false
- name: Run actionlint
uses: raven-actions/actionlint@205b530c5d9fa8f44ae9ed59f341a0db994aa6f8 # v2
Generated
+57 -116
View File
@@ -38,7 +38,7 @@ version = "1.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
dependencies = [
"windows-sys 0.61.2",
"windows-sys",
]
[[package]]
@@ -49,7 +49,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
dependencies = [
"anstyle",
"once_cell_polyfill",
"windows-sys 0.61.2",
"windows-sys",
]
[[package]]
@@ -60,11 +60,11 @@ checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3"
[[package]]
name = "block-buffer"
version = "0.10.4"
version = "0.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
checksum = "cdd35008169921d80bc60d3d0ab416eecb028c4cd653352907921d95084790be"
dependencies = [
"generic-array",
"hybrid-array",
]
[[package]]
@@ -128,6 +128,12 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"
[[package]]
name = "cmov"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f88a43d011fc4a6876cb7344703e297c71dda42494fee094d5f7c76bf13f746"
[[package]]
name = "colorchoice"
version = "1.0.5"
@@ -135,13 +141,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
[[package]]
name = "crypto-common"
version = "0.1.7"
name = "const-oid"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"
checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c"
[[package]]
name = "crypto-common"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77727bb15fa921304124b128af125e7e3b968275d1b108b379190264f4423710"
dependencies = [
"generic-array",
"typenum",
"hybrid-array",
]
[[package]]
@@ -152,18 +163,28 @@ checksum = "e0b1fab2ae45819af2d0731d60f2afe17227ebb1a1538a236da84c93e9a60162"
dependencies = [
"dispatch2",
"nix",
"windows-sys 0.61.2",
"windows-sys",
]
[[package]]
name = "ctutils"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d5515a3834141de9eafb9717ad39eea8247b5674e6066c404e8c4b365d2a29e"
dependencies = [
"cmov",
]
[[package]]
name = "digest"
version = "0.10.7"
version = "0.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2"
dependencies = [
"block-buffer",
"const-oid",
"crypto-common",
"subtle",
"ctutils",
]
[[package]]
@@ -178,16 +199,6 @@ dependencies = [
"objc2",
]
[[package]]
name = "generic-array"
version = "0.14.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
dependencies = [
"typenum",
"version_check",
]
[[package]]
name = "heck"
version = "0.5.0"
@@ -196,16 +207,25 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "hmac"
version = "0.12.1"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
checksum = "6303bc9732ae41b04cb554b844a762b4115a61bfaa81e3e83050991eeb56863f"
dependencies = [
"digest",
]
[[package]]
name = "hybrid-array"
version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9155a582abd142abc056962c29e3ce5ff2ad5469f4246b537ed42c5deba857da"
dependencies = [
"typenum",
]
[[package]]
name = "irtt-cli"
version = "0.1.0"
version = "0.2.0"
dependencies = [
"clap",
"ctrlc",
@@ -216,7 +236,7 @@ dependencies = [
[[package]]
name = "irtt-client"
version = "0.1.0"
version = "0.2.0"
dependencies = [
"irtt-proto",
"libc",
@@ -226,7 +246,7 @@ dependencies = [
[[package]]
name = "irtt-proto"
version = "0.1.0"
version = "0.2.0"
dependencies = [
"hmac",
"md-5",
@@ -236,7 +256,7 @@ dependencies = [
[[package]]
name = "irtt-stats"
version = "0.1.0"
version = "0.2.0"
dependencies = [
"irtt-client",
]
@@ -255,9 +275,9 @@ checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
[[package]]
name = "md-5"
version = "0.10.6"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf"
checksum = "69b6441f590336821bb897fb28fc622898ccceb1d6cea3fde5ea86b090c4de98"
dependencies = [
"cfg-if",
"digest",
@@ -265,9 +285,9 @@ dependencies = [
[[package]]
name = "nix"
version = "0.31.2"
version = "0.31.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d6d0705320c1e6ba1d912b5e37cf18071b6c2e9b7fa8215a1e8a7651966f5d3"
checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d"
dependencies = [
"bitflags",
"cfg-if",
@@ -316,12 +336,12 @@ dependencies = [
[[package]]
name = "socket2"
version = "0.5.10"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678"
checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e"
dependencies = [
"libc",
"windows-sys 0.52.0",
"windows-sys",
]
[[package]]
@@ -385,27 +405,12 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "version_check"
version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]]
name = "windows-link"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "windows-sys"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
dependencies = [
"windows-targets",
]
[[package]]
name = "windows-sys"
version = "0.61.2"
@@ -414,67 +419,3 @@ checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
dependencies = [
"windows-link",
]
[[package]]
name = "windows-targets"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
dependencies = [
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_gnullvm",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
[[package]]
name = "windows_aarch64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
[[package]]
name = "windows_i686_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
[[package]]
name = "windows_i686_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
[[package]]
name = "windows_i686_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
[[package]]
name = "windows_x86_64_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
[[package]]
name = "windows_x86_64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
+15 -3
View File
@@ -16,6 +16,18 @@ homepage = "https://github.com/Lochnair/irtt-rs"
readme = "README.md"
[workspace.dependencies]
irtt-proto = { version = "0.1.0", path = "crates/irtt-proto" }
irtt-client = { version = "0.1.0", path = "crates/irtt-client" }
irtt-stats = { version = "0.1.0", path = "crates/irtt-stats" }
irtt-proto = { version = "0.2.0", path = "crates/irtt-proto" }
irtt-client = { version = "0.2.0", path = "crates/irtt-client" }
irtt-stats = { version = "0.2.0", path = "crates/irtt-stats" }
[profile.release]
opt-level = "z"
lto = true
codegen-units = 1
panic = "abort"
strip = "symbols"
# The profile that 'dist' will build with
[profile.dist]
inherits = "release"
lto = "thin"
+3 -3
View File
@@ -1,6 +1,6 @@
[package]
name = "irtt-cli"
version = "0.1.0"
version = "0.2.0"
description = "Command-line IRTT-compatible client"
edition.workspace = true
license.workspace = true
@@ -16,8 +16,8 @@ default = ["stats"]
stats = ["dep:irtt-stats"]
[dependencies]
clap = { version = "4.5", features = ["derive"] }
ctrlc = "3.4"
clap = { version = "4.6.1", features = ["derive"] }
ctrlc = "3.5.2"
irtt-client.workspace = true
irtt-proto.workspace = true
irtt-stats = { workspace = true, optional = true }
+38 -79
View File
@@ -326,8 +326,8 @@ pub struct HumanEventStats {
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct HumanIpdvPair {
pub previous_logical_seq: u64,
pub current_logical_seq: u64,
pub previous_seq: u32,
pub current_seq: u32,
pub rtt_ipdv: Duration,
pub send_ipdv: Option<Duration>,
pub receive_ipdv: Option<Duration>,
@@ -352,8 +352,8 @@ impl From<irtt_stats::EventStatsUpdate> for HumanEventStats {
impl From<irtt_stats::IpdvPairUpdate> for HumanIpdvPair {
fn from(value: irtt_stats::IpdvPairUpdate) -> Self {
Self {
previous_logical_seq: value.previous_logical_seq,
current_logical_seq: value.current_logical_seq,
previous_seq: value.previous_seq,
current_seq: value.current_seq,
rtt_ipdv: value.rtt_ipdv,
send_ipdv: value.send_ipdv,
receive_ipdv: value.receive_ipdv,
@@ -385,7 +385,6 @@ pub fn format_human_event_with_options(
ClientEvent::EchoSent { .. } => String::new(),
ClientEvent::EchoReply {
seq,
logical_seq,
rtt,
server_timing,
one_way,
@@ -393,17 +392,9 @@ pub fn format_human_event_with_options(
..
} => {
let mut out = format!("seq={seq}");
if options.verbose {
write!(out, " logical_seq={logical_seq}").unwrap();
}
write!(out, " rtt={}", format_duration(rtt.effective)).unwrap();
write_human_one_way(&mut out, *one_way);
write!(
out,
" ipdv={}",
format_human_ipdv(stats, Some(*logical_seq))
)
.unwrap();
write!(out, " ipdv={}", format_human_ipdv(stats, *seq)).unwrap();
if let Some(processing) = server_timing.and_then(|timing| timing.processing) {
write!(out, " proc={}", format_duration(processing)).unwrap();
}
@@ -412,17 +403,14 @@ pub fn format_human_event_with_options(
}
out
}
ClientEvent::EchoLoss {
seq, logical_seq, ..
} => {
format!("loss seq={seq} logical_seq={logical_seq}")
ClientEvent::EchoLoss { seq, .. } => {
format!("loss seq={seq}")
}
ClientEvent::DuplicateReply { seq, remote, .. } => {
format!("duplicate seq={seq} remote={remote}")
}
ClientEvent::LateReply {
seq,
logical_seq,
highest_seen,
remote,
rtt,
@@ -430,19 +418,16 @@ pub fn format_human_event_with_options(
received_stats,
..
} => {
let mut out = format!(
"late seq={seq} logical_seq={} highest_seen={highest_seen} remote={remote}",
optional_u64(*logical_seq)
);
let mut out = format!("late seq={seq} highest_seen={highest_seen} remote={remote}",);
if let Some(rtt) = rtt {
write!(out, " rtt={}", format_duration(rtt.effective)).unwrap();
write_human_one_way(&mut out, *one_way);
write!(out, " ipdv={}", format_human_ipdv(stats, *logical_seq)).unwrap();
write!(out, " ipdv={}", format_human_ipdv(stats, *seq)).unwrap();
}
write_human_received_stats(&mut out, *received_stats);
out
}
ClientEvent::Warning { kind, message } => {
ClientEvent::Warning { kind, message, .. } => {
format!("warning kind={} message={message}", warning_kind(*kind))
}
}
@@ -489,7 +474,6 @@ fn format_machine(event: &ClientEvent) -> String {
ClientEvent::EchoSent { .. } => {}
ClientEvent::EchoReply {
seq,
logical_seq,
remote,
sent_at,
received_at,
@@ -501,7 +485,7 @@ fn format_machine(event: &ClientEvent) -> String {
packet_meta,
} => {
write_common(&mut out, "echo_reply");
write_seq(&mut out, *seq, Some(*logical_seq));
write_seq(&mut out, *seq);
write_remote(&mut out, *remote);
write_wall(&mut out, "client_send_wall_ns", sent_at.wall);
write_wall(&mut out, "client_receive_wall_ns", received_at.wall);
@@ -511,14 +495,9 @@ fn format_machine(event: &ClientEvent) -> String {
write_received_stats(&mut out, *received_stats);
write_packet_meta(&mut out, *packet_meta);
}
ClientEvent::EchoLoss {
seq,
logical_seq,
sent_at,
..
} => {
ClientEvent::EchoLoss { seq, sent_at, .. } => {
write_common(&mut out, "loss");
write_seq(&mut out, *seq, Some(*logical_seq));
write_seq(&mut out, *seq);
write_wall(&mut out, "client_send_wall_ns", sent_at.wall);
out.push_str(" warning=loss");
}
@@ -529,14 +508,13 @@ fn format_machine(event: &ClientEvent) -> String {
bytes: _,
} => {
write_common(&mut out, "duplicate");
write_seq(&mut out, *seq, None);
write_seq(&mut out, *seq);
write_remote(&mut out, *remote);
write_wall(&mut out, "client_receive_wall_ns", received_at.wall);
out.push_str(" warning=duplicate");
}
ClientEvent::LateReply {
seq,
logical_seq,
highest_seen,
remote,
sent_at,
@@ -549,7 +527,7 @@ fn format_machine(event: &ClientEvent) -> String {
packet_meta,
} => {
write_common(&mut out, "late");
write_seq(&mut out, *seq, *logical_seq);
write_seq(&mut out, *seq);
write_remote(&mut out, *remote);
write!(out, " highest_seen={highest_seen}").unwrap();
if let Some(sent_at) = sent_at {
@@ -565,8 +543,9 @@ fn format_machine(event: &ClientEvent) -> String {
write_packet_meta(&mut out, *packet_meta);
out.push_str(" warning=late");
}
ClientEvent::Warning { kind, message } => {
ClientEvent::Warning { kind, message, at } => {
write_common(&mut out, "warning");
write_wall(&mut out, "at", at.wall);
write!(
out,
" warning_kind={} message={}",
@@ -593,7 +572,6 @@ fn format_simple(event: &ClientEvent) -> String {
ClientEvent::EchoSent { .. } => String::new(),
ClientEvent::EchoReply {
seq,
logical_seq,
remote,
rtt,
server_timing,
@@ -601,7 +579,7 @@ fn format_simple(event: &ClientEvent) -> String {
..
} => {
let mut out = format!(
"reply seq={seq} logical_seq={logical_seq} remote={remote} rtt_us={}",
"reply seq={seq} remote={remote} rtt_us={}",
duration_us(rtt.effective)
);
if let Some(raw) = rtt.adjusted.map(|_| rtt.raw) {
@@ -612,32 +590,26 @@ fn format_simple(event: &ClientEvent) -> String {
}
out
}
ClientEvent::EchoLoss {
seq, logical_seq, ..
} => {
format!("loss seq={seq} logical_seq={logical_seq}")
ClientEvent::EchoLoss { seq, .. } => {
format!("loss seq={seq}")
}
ClientEvent::DuplicateReply { seq, remote, .. } => {
format!("duplicate seq={seq} remote={remote}")
}
ClientEvent::LateReply {
seq,
logical_seq,
highest_seen,
remote,
rtt,
..
} => {
let mut out = format!(
"late seq={seq} logical_seq={} highest_seen={highest_seen} remote={remote}",
optional_u64(*logical_seq)
);
let mut out = format!("late seq={seq} highest_seen={highest_seen} remote={remote}",);
if let Some(rtt) = rtt {
write!(out, " rtt_us={}", duration_us(rtt.effective)).unwrap();
}
out
}
ClientEvent::Warning { kind, message } => {
ClientEvent::Warning { kind, message, .. } => {
format!("warning kind={} message={message}", warning_kind(*kind))
}
}
@@ -647,11 +619,8 @@ fn write_common(out: &mut String, event: &str) {
write!(out, "event={event}").unwrap();
}
fn write_seq(out: &mut String, seq: u32, logical_seq: Option<u64>) {
fn write_seq(out: &mut String, seq: u32) {
write!(out, " seq={seq}").unwrap();
if let Some(logical_seq) = logical_seq {
write!(out, " logical_seq={logical_seq}").unwrap();
}
}
fn write_remote(out: &mut String, remote: SocketAddr) {
@@ -762,19 +731,21 @@ fn write_human_received_stats(out: &mut String, stats: Option<ReceivedStatsSampl
}
}
fn format_human_ipdv(stats: Option<&HumanEventStats>, logical_seq: Option<u64>) -> String {
fn format_human_ipdv(stats: Option<&HumanEventStats>, seq: u32) -> String {
let Some(stats) = stats else {
return "n/a".to_owned();
};
let pair = logical_seq
.and_then(|seq| {
let pair = stats
.ipdv_pairs
.iter()
.find(|pair| pair.current_seq == seq)
.or_else(|| {
stats
.ipdv_pairs
.iter()
.find(|pair| pair.current_logical_seq == seq)
})
.or_else(|| stats.ipdv_pairs.last());
.find(|pair| pair.previous_seq == seq)
});
pair.map(|pair| format_duration(pair.rtt_ipdv))
.unwrap_or_else(|| "n/a".to_owned())
@@ -819,17 +790,12 @@ fn format_ns(ns: f64) -> String {
}
}
fn optional_u64(value: Option<u64>) -> String {
value
.map(|value| value.to_string())
.unwrap_or_else(|| "none".to_owned())
}
fn warning_kind(kind: WarningKind) -> &'static str {
match kind {
WarningKind::MalformedOrUnrelatedPacket => "malformed_or_unrelated_packet",
WarningKind::WrongToken => "wrong_token",
WarningKind::UntrackedReply => "untracked_reply",
_ => "unknown",
}
}
@@ -877,7 +843,6 @@ mod tests {
fn reply_event_with_meta(packet_meta: PacketMeta) -> ClientEvent {
ClientEvent::EchoReply {
seq: 7,
logical_seq: 8,
remote: test_remote(),
sent_at: test_timestamp(Duration::from_secs(1)),
received_at: test_timestamp(Duration::from_secs(1) + Duration::from_micros(1500)),
@@ -913,7 +878,6 @@ mod tests {
fn late_event_with_meta(packet_meta: PacketMeta) -> ClientEvent {
ClientEvent::LateReply {
seq: 4,
logical_seq: None,
highest_seen: 9,
remote: test_remote(),
sent_at: None,
@@ -1205,6 +1169,7 @@ mod tests {
&ClientEvent::Warning {
kind: WarningKind::WrongToken,
message: "bad".to_owned(),
at: ClientTimestamp::now()
},
OutputMode::RttUs
),
@@ -1217,7 +1182,6 @@ mod tests {
let line = format_event(&reply_event(), OutputMode::Machine).unwrap();
assert!(line.starts_with("event=echo_reply "));
assert!(line.contains("seq=7"));
assert!(line.contains("logical_seq=8"));
assert!(line.contains("remote=127.0.0.1:2112"));
assert!(line.contains("client_send_wall_ns=1000000000"));
assert!(line.contains("client_receive_wall_ns=1001500000"));
@@ -1326,7 +1290,7 @@ mod tests {
let line = format_event(&reply_event(), OutputMode::Simple).unwrap();
assert_eq!(
line,
"reply seq=7 logical_seq=8 remote=127.0.0.1:2112 rtt_us=1200 raw_rtt_us=1500 server_processing_us=300"
"reply seq=7 remote=127.0.0.1:2112 rtt_us=1200 raw_rtt_us=1500 server_processing_us=300"
);
}
@@ -1338,7 +1302,6 @@ mod tests {
assert!(line.contains("sd=400.0µs"));
assert!(line.contains("ipdv=n/a"));
assert!(line.contains("proc=300.0µs"));
assert!(!line.contains("logical_seq="));
assert!(!line.contains("server_received="));
assert!(!line.contains("server_window="));
assert!(!line.contains("rtt_us="));
@@ -1360,7 +1323,6 @@ mod tests {
HumanOutputOptions { verbose: true },
);
assert!(line.contains("logical_seq=8"));
assert!(line.contains("server_received=9"));
assert!(line.contains("server_window=0x7"));
}
@@ -1372,8 +1334,8 @@ mod tests {
Some(HumanEventStats {
contributed_sample: true,
ipdv_pairs: vec![HumanIpdvPair {
previous_logical_seq: 7,
current_logical_seq: 8,
previous_seq: 7,
current_seq: 8,
rtt_ipdv: Duration::from_micros(47),
send_ipdv: None,
receive_ipdv: None,
@@ -1388,7 +1350,6 @@ mod tests {
fn human_reply_marks_missing_one_way_delay_unavailable() {
let ClientEvent::EchoReply {
seq,
logical_seq,
remote,
sent_at,
received_at,
@@ -1404,7 +1365,6 @@ mod tests {
};
let event = ClientEvent::EchoReply {
seq,
logical_seq,
remote,
sent_at,
received_at,
@@ -1427,7 +1387,6 @@ mod tests {
let ts = test_timestamp(Duration::from_secs(1));
let event = ClientEvent::EchoSent {
seq: 1,
logical_seq: 2,
remote: test_remote(),
scheduled_at: ts.mono,
sent_at: ts,
@@ -1448,7 +1407,6 @@ mod tests {
let events = [
ClientEvent::EchoLoss {
seq: 1,
logical_seq: 2,
sent_at: ts,
timeout_at: ts.mono + Duration::from_secs(1),
},
@@ -1460,7 +1418,6 @@ mod tests {
},
ClientEvent::LateReply {
seq: 4,
logical_seq: None,
highest_seen: 9,
remote: test_remote(),
sent_at: None,
@@ -1475,6 +1432,7 @@ mod tests {
ClientEvent::Warning {
kind: WarningKind::UntrackedReply,
message: "untracked reply".to_owned(),
at: ClientTimestamp::now(),
},
];
@@ -1498,6 +1456,7 @@ mod tests {
interval_ns: 1_000_000_000,
..Params::default()
},
restrictions: vec![],
},
at: test_timestamp(Duration::from_secs(1)),
};
+20 -20
View File
@@ -13,7 +13,11 @@ use clap::Parser;
use irtt_cli::{
format_event, format_human_event_with_options, CliArgs, HumanOutputOptions, OutputMode,
};
use irtt_client::{Client, ClientEvent, ClientTimestamp, OpenOutcome, RecvBudget};
use irtt_client::{Client, ClientEvent, OpenOutcome, RecvBudget};
#[cfg(test)]
use irtt_client::ClientTimestamp;
#[cfg(feature = "stats")]
use irtt_stats::{StatsCollector, StatsConfig};
@@ -72,7 +76,7 @@ fn run(args: CliArgs, shutdown_requested: &AtomicBool) -> Result<(), Box<dyn std
return Ok(());
}
let open = client.open(ClientTimestamp::now())?;
let open = client.open()?;
output.print_event(open_event(&open))?;
let mut interrupted = false;
@@ -85,7 +89,7 @@ fn run(args: CliArgs, shutdown_requested: &AtomicBool) -> Result<(), Box<dyn std
let events = client.recv_available(RECV_BUDGET)?;
output.print_events(&events)?;
let events = client.poll_timeouts(ClientTimestamp::now())?;
let events = client.poll_timeouts()?;
output.print_events(&events)?;
if is_shutdown_requested(shutdown_requested) {
@@ -105,10 +109,10 @@ fn run(args: CliArgs, shutdown_requested: &AtomicBool) -> Result<(), Box<dyn std
drain_final_replies(&mut client, &mut output)?;
}
let events = client.poll_timeouts(ClientTimestamp::now())?;
let events = client.poll_timeouts()?;
output.print_events(&events)?;
let events = client.close(ClientTimestamp::now())?;
let events = client.close()?;
output.print_events(&events)?;
output.print_summary()?;
output.out.flush()?;
@@ -154,7 +158,7 @@ fn drain_final_replies<W: Write>(
printed |= !events.is_empty();
output.print_events(&events)?;
let events = client.poll_timeouts(ClientTimestamp::now())?;
let events = client.poll_timeouts()?;
printed |= !events.is_empty();
output.print_events(&events)?;
@@ -233,7 +237,7 @@ impl<W: Write> EventOutput<'_, W> {
self.out,
"{}",
irtt_cli::summary::format_summary_with_options(
&self.stats.summary(),
&self.stats.snapshot(),
irtt_cli::summary::SummaryFormatOptions {
verbose: self.human_options.verbose,
},
@@ -309,15 +313,14 @@ mod tests {
SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 2112)
}
fn reply_event(logical_seq: u64, rtt_us: u64) -> ClientEvent {
let sent_at = test_timestamp(Duration::from_millis(logical_seq));
fn reply_event(seq: u32, rtt_us: u64) -> ClientEvent {
let sent_at = test_timestamp(Duration::from_millis(seq as u64));
let received_at = ClientTimestamp {
wall: sent_at.wall + Duration::from_micros(rtt_us),
mono: sent_at.mono + Duration::from_micros(rtt_us),
};
ClientEvent::EchoReply {
seq: logical_seq as u32,
logical_seq,
seq,
remote: test_remote(),
sent_at,
received_at,
@@ -345,7 +348,6 @@ mod tests {
let events = [
ClientEvent::EchoSent {
seq: 1,
logical_seq: 1,
remote: test_remote(),
scheduled_at: sent_at.mono,
sent_at,
@@ -355,7 +357,6 @@ mod tests {
},
ClientEvent::EchoReply {
seq: 1,
logical_seq: 1,
remote: test_remote(),
sent_at,
received_at,
@@ -389,7 +390,7 @@ mod tests {
}
let rendered = String::from_utf8(out).unwrap();
let summary = stats.summary();
let summary = stats.snapshot();
assert_eq!(rendered, "1200\n");
assert_eq!(summary.packets.packets_sent, 1);
assert_eq!(summary.packets.unique_replies, 1);
@@ -438,12 +439,11 @@ mod tests {
#[test]
fn continuous_mode_uses_continuous_stats_config() {
let mut collector = StatsCollector::new(stats_config(true));
for seq in 0..5000 {
let sent_at = test_timestamp(Duration::from_micros(seq));
let received_at = test_timestamp(Duration::from_micros(seq + 1));
for seq in 0..5000_u32 {
let sent_at = test_timestamp(Duration::from_micros(seq as u64));
let received_at = test_timestamp(Duration::from_micros(seq as u64 + 1));
collector.process(&ClientEvent::EchoReply {
seq: seq as u32,
logical_seq: seq,
seq,
remote: test_remote(),
sent_at,
received_at,
@@ -462,7 +462,7 @@ mod tests {
});
}
assert_eq!(collector.summary().rtt.primary.median_ns, None);
assert_eq!(collector.snapshot().rtt.primary.median_ns, None);
}
#[test]
+24 -74
View File
@@ -1,10 +1,8 @@
use std::fmt::Write as _;
use irtt_stats::{
DurationStats, DurationStatsWithMedian, FiniteSummary, SignedDurationStatsWithMedian,
};
use irtt_stats::{Snapshot, TimeStats};
pub fn format_summary(summary: &FiniteSummary) -> String {
pub fn format_summary(summary: &Snapshot) -> String {
format_summary_with_options(summary, SummaryFormatOptions::default())
}
@@ -13,10 +11,7 @@ pub struct SummaryFormatOptions {
pub verbose: bool,
}
pub fn format_summary_with_options(
summary: &FiniteSummary,
options: SummaryFormatOptions,
) -> String {
pub fn format_summary_with_options(summary: &Snapshot, options: SummaryFormatOptions) -> String {
let mut out = String::new();
let packets = summary.packets;
let loss = summary.loss;
@@ -32,27 +27,27 @@ pub fn format_summary_with_options(
.unwrap();
writeln!(out, " {}", "-".repeat(82)).unwrap();
write_signed_duration_row(&mut out, "RTT", &summary.rtt.primary);
write_time_row(&mut out, "RTT", &summary.rtt.primary);
if options.verbose {
write_duration_row(&mut out, "raw RTT", &summary.rtt.raw);
write_signed_duration_row(&mut out, "adjusted RTT", &summary.rtt.adjusted);
write_time_row(&mut out, "raw RTT", &summary.rtt.raw);
write_time_row(&mut out, "adjusted RTT", &summary.rtt.adjusted);
}
write_duration_row(&mut out, "IPDV/jitter", &summary.ipdv.round_trip);
write_duration_row(&mut out, "send IPDV", &summary.ipdv.send);
write_duration_row(&mut out, "receive IPDV", &summary.ipdv.receive);
write_duration_row(&mut out, "send delay", &summary.one_way_delay.send_delay);
write_duration_row(
write_time_row(&mut out, "IPDV/jitter", &summary.ipdv.round_trip);
write_time_row(&mut out, "send IPDV", &summary.ipdv.send);
write_time_row(&mut out, "receive IPDV", &summary.ipdv.receive);
write_time_row(&mut out, "send delay", &summary.one_way_delay.send_delay);
write_time_row(
&mut out,
"receive delay",
&summary.one_way_delay.receive_delay,
);
write_duration_row_no_median(
write_time_row(
&mut out,
"server processing",
&summary.server_processing.processing,
);
write_duration_row_no_median(&mut out, "send call", &summary.send_call);
write_duration_row_no_median(&mut out, "timer error", &summary.timer_error);
write_time_row(&mut out, "send call", &summary.send_call);
write_time_row(&mut out, "timer error", &summary.timer_error);
writeln!(out).unwrap();
writeln!(
@@ -97,24 +92,7 @@ pub fn format_summary_with_options(
out
}
fn write_duration_row(out: &mut String, label: &str, value: &DurationStatsWithMedian) {
if value.stats.count == 0 {
return;
}
writeln!(
out,
" {label:<18} {:>7} {:>10} {:>10} {:>10} {:>10} {:>10}",
value.stats.count,
format_ns_u64(value.stats.min_ns),
format_ns_f64(value.stats.mean_ns),
format_ns_f64_opt(value.median_ns),
format_ns_u64(value.stats.max_ns),
format_ns_f64(value.stddev_ns())
)
.unwrap();
}
fn write_duration_row_no_median(out: &mut String, label: &str, value: &DurationStats) {
fn write_time_row(out: &mut String, label: &str, value: &TimeStats) {
if value.count == 0 {
return;
}
@@ -122,27 +100,10 @@ fn write_duration_row_no_median(out: &mut String, label: &str, value: &DurationS
out,
" {label:<18} {:>7} {:>10} {:>10} {:>10} {:>10} {:>10}",
value.count,
format_ns_u64(value.min_ns),
format_ns_i128(value.min_ns),
format_ns_f64(value.mean_ns),
"-",
format_ns_u64(value.max_ns),
format_ns_f64(value.stddev_ns())
)
.unwrap();
}
fn write_signed_duration_row(out: &mut String, label: &str, value: &SignedDurationStatsWithMedian) {
if value.stats.count == 0 {
return;
}
writeln!(
out,
" {label:<18} {:>7} {:>10} {:>10} {:>10} {:>10} {:>10}",
value.stats.count,
format_ns_i128(value.stats.min_ns),
format_ns_f64(value.stats.mean_ns),
format_ns_f64_opt(value.median_ns),
format_ns_i128(value.stats.max_ns),
format_ns_i128(value.max_ns),
format_ns_f64(value.stddev_ns())
)
.unwrap();
@@ -152,12 +113,6 @@ fn format_percent(value: f64) -> String {
format!("{value:.2}%")
}
fn format_ns_u64(value: Option<u64>) -> String {
value
.map(|value| format_ns_f64(value as f64))
.unwrap_or_else(|| "-".to_owned())
}
fn format_ns_i128(value: Option<i128>) -> String {
value
.map(|value| format_ns_f64(value as f64))
@@ -188,7 +143,7 @@ mod tests {
use irtt_client::{
ClientEvent, ClientTimestamp, PacketMeta, RttSample, ServerTiming, SignedDuration,
};
use irtt_stats::{SignedDurationStats, StatsCollector, StatsConfig};
use irtt_stats::{StatsCollector, StatsConfig};
use std::{
net::{IpAddr, Ipv4Addr, SocketAddr},
time::{Duration, Instant, UNIX_EPOCH},
@@ -207,7 +162,7 @@ mod tests {
#[test]
fn empty_summary_omits_optional_metric_sections() {
let summary = StatsCollector::new(StatsConfig::finite()).summary();
let summary = StatsCollector::new(StatsConfig::finite()).snapshot();
let output = format_summary(&summary);
assert!(output.contains("Metric"));
@@ -229,7 +184,6 @@ mod tests {
collector.process(&ClientEvent::EchoSent {
seq: 1,
logical_seq: 1,
remote: test_remote(),
scheduled_at: sent_at.mono,
sent_at,
@@ -239,7 +193,6 @@ mod tests {
});
collector.process(&ClientEvent::EchoReply {
seq: 1,
logical_seq: 1,
remote: test_remote(),
sent_at,
received_at,
@@ -265,7 +218,7 @@ mod tests {
packet_meta: PacketMeta::default(),
});
let output = format_summary(&collector.summary());
let output = format_summary(&collector.snapshot());
assert!(output.contains("packets: sent=1 received=1 unique=1 lost=0 loss=0.00%"));
assert!(output.contains("bytes: sent=64 received=64"));
@@ -287,7 +240,6 @@ mod tests {
let received_at = test_timestamp(Duration::from_secs(1) + Duration::from_micros(1500));
collector.process(&ClientEvent::EchoReply {
seq: 1,
logical_seq: 1,
remote: test_remote(),
sent_at: test_timestamp(Duration::from_secs(1)),
received_at,
@@ -306,7 +258,7 @@ mod tests {
});
let output = format_summary_with_options(
&collector.summary(),
&collector.snapshot(),
SummaryFormatOptions { verbose: true },
);
@@ -316,20 +268,18 @@ mod tests {
#[test]
fn signed_stats_can_format_negative_values() {
let stats = SignedDurationStatsWithMedian {
stats: SignedDurationStats {
let stats = TimeStats {
count: 1,
total_ns: -500,
min_ns: Some(-500),
max_ns: Some(-500),
mean_ns: -500.0,
variance_ns2: 0.0,
},
median_ns: Some(-500.0),
variance_ns2: 0.0,
};
let mut out = String::new();
write_signed_duration_row(&mut out, "RTT", &stats);
write_time_row(&mut out, "RTT", &stats);
assert!(out.contains("-500ns"));
}
}
+4 -4
View File
@@ -1,6 +1,6 @@
[package]
name = "irtt-client"
version = "0.1.0"
version = "0.2.0"
description = "IRTT-compatible Rust client library"
edition.workspace = true
license.workspace = true
@@ -13,9 +13,9 @@ categories = ["network-programming"]
[dependencies]
irtt-proto.workspace = true
libc = { version = "0.2", optional = true }
socket2 = { version = "0.5", features = ["all"] }
thiserror = "2"
libc = { version = "0.2.186", optional = true }
socket2 = { version = "0.6.3", features = ["all"] }
thiserror = "2.0.18"
[features]
default = []
+57 -56
View File
@@ -23,7 +23,7 @@ use crate::{
metadata::ReceiveMeta,
probe::{CompletedSet, PendingMap, PendingProbe, TimedOutMap},
receive::recv_datagram,
session::{validate_negotiated_params, ActiveSession, ClientPhase, NegotiatedParams},
session::{negotiate_params, ActiveSession, ClientPhase, NegotiatedParams},
socket::{connect_udp_socket, resolve_remote, validate_open_timeouts},
socket_options::{apply_dscp_to_socket, clear_dscp_on_socket},
timing::ClientTimestamp,
@@ -73,7 +73,7 @@ impl Client {
})
}
pub fn open(&mut self, now: ClientTimestamp) -> Result<OpenOutcome, ClientError> {
pub fn open(&mut self) -> Result<OpenOutcome, ClientError> {
match self.phase {
ClientPhase::Connected => {}
ClientPhase::Open { .. } => return Err(ClientError::AlreadyOpen),
@@ -81,23 +81,12 @@ impl Client {
ClientPhase::NoTestCompleted => return Err(ClientError::AlreadyCompleted),
}
let outcome = self.open_inner(now);
let restore = self
.socket
.set_read_timeout(self.config.socket_config.recv_timeout);
match (outcome, restore) {
(Ok(outcome), Ok(())) => Ok(outcome),
(Ok(_), Err(source)) => Err(ClientError::ReadTimeoutRestore { source }),
(Err(err), Ok(())) => Err(err),
(Err(err), Err(_)) => Err(err),
}
}
fn open_inner(&mut self, _now: ClientTimestamp) -> Result<OpenOutcome, ClientError> {
let outcome = (|| -> Result<OpenOutcome, ClientError> {
let request = OpenRequest {
params: self.requested.clone(),
close: self.config.run_mode == RunMode::NoTest,
};
let packet = encode_open_request(&request, self.config.hmac_key.as_deref())?;
let mut buf = [0_u8; MAX_OPEN_PACKET_SIZE];
@@ -111,6 +100,7 @@ impl Client {
&buf[..size],
self.config.hmac_key.as_deref(),
)?;
return self.accept_open_reply(reply, ClientTimestamp::now());
}
Err(err)
@@ -123,9 +113,21 @@ impl Client {
}
Err(ClientError::OpenTimeout)
})();
let restore = self
.socket
.set_read_timeout(self.config.socket_config.recv_timeout);
match (outcome, restore) {
(Ok(outcome), Ok(())) => Ok(outcome),
(Ok(_), Err(source)) => Err(ClientError::ReadTimeoutRestore { source }),
(Err(err), Ok(())) => Err(err),
(Err(err), Err(_)) => Err(err),
}
}
pub fn close(&mut self, now: ClientTimestamp) -> Result<Vec<ClientEvent>, ClientError> {
pub fn close(&mut self) -> Result<Vec<ClientEvent>, ClientError> {
let token = match self.phase {
ClientPhase::Open { token } => token,
ClientPhase::Closed => return Err(ClientError::AlreadyClosed),
@@ -147,7 +149,7 @@ impl Client {
Ok(vec![ClientEvent::SessionClosed {
remote: self.remote,
token,
at: now,
at: ClientTimestamp::now(),
}])
}
@@ -208,7 +210,6 @@ impl Client {
.expect("negotiated must exist when Open");
let wire_seq = session.next_wire_seq;
let logical_seq = session.next_logical_seq;
let scheduled_at = session.next_send_at;
let request = EchoRequest {
@@ -230,7 +231,6 @@ impl Client {
.expect("session must exist when phase is Open");
let pending = PendingProbe {
logical_seq,
wire_seq,
sent_at,
timeout_at: sent_at
@@ -241,13 +241,6 @@ impl Client {
session.pending.insert(pending)?;
session.next_wire_seq = session.next_wire_seq.wrapping_add(1);
session.next_logical_seq =
session
.next_logical_seq
.checked_add(1)
.ok_or(ClientError::CounterOverflow {
counter: "next_logical_seq",
})?;
session.packets_sent =
session
.packets_sent
@@ -277,7 +270,6 @@ impl Client {
Ok(vec![ClientEvent::EchoSent {
seq: wire_seq,
logical_seq,
remote: self.remote,
scheduled_at,
sent_at,
@@ -317,6 +309,7 @@ impl Client {
return Ok(vec![ClientEvent::Warning {
kind: WarningKind::MalformedOrUnrelatedPacket,
message: "dropped malformed or unrelated packet".to_owned(),
at: now,
}]);
};
self.process_echo_reply(reply, packet_len, now, datagram.meta)
@@ -335,11 +328,24 @@ impl Client {
}
#[cfg(test)]
fn recv_buffer_size(&self) -> usize {
pub(crate) fn recv_buffer_size(&self) -> usize {
recv_buffer_size(self.config.hmac_key.is_some(), self.negotiated.as_ref())
.expect("test params must have a non-negative packet length")
}
pub fn poll_timeouts(&mut self, now: ClientTimestamp) -> Result<Vec<ClientEvent>, ClientError> {
/// Polls for probes that have timed out as of the current monotonic time.
pub fn poll_timeouts(&mut self) -> Result<Vec<ClientEvent>, ClientError> {
self.poll_timeouts_at(Instant::now())
}
/// Polls for probes that have timed out as of `now`.
///
/// This is useful for callers that drive `Client` from their own event loop and
/// want timeout decisions to use the same sampled `Instant` as their scheduling
/// logic.
///
/// `now` is monotonic time only; wall-clock time is not used for timeout expiry.
pub fn poll_timeouts_at(&mut self, now: Instant) -> Result<Vec<ClientEvent>, ClientError> {
match self.phase {
ClientPhase::Open { .. } => {}
ClientPhase::Closed => return Err(ClientError::AlreadyClosed),
@@ -352,12 +358,11 @@ impl Client {
.as_mut()
.expect("session must exist when phase is Open");
let expired = session.pending.drain_expired(now.mono);
let expired = session.pending.drain_expired(now);
let mut events = Vec::with_capacity(expired.len());
for probe in expired {
events.push(ClientEvent::EchoLoss {
seq: probe.wire_seq,
logical_seq: probe.logical_seq,
sent_at: probe.sent_at,
timeout_at: probe.timeout_at,
});
@@ -401,6 +406,7 @@ impl Client {
return Ok(vec![ClientEvent::Warning {
kind: WarningKind::MalformedOrUnrelatedPacket,
message: "dropped malformed or unrelated packet".to_owned(),
at: now,
}]);
};
self.process_echo_reply(reply, packet_len, now, meta)
@@ -433,6 +439,7 @@ impl Client {
"dropped reply with wrong token: expected {token:#x}, got {:#x}",
reply.token
),
at: now,
}]);
}
@@ -451,13 +458,12 @@ impl Client {
let highest_seen = session.highest_received_seq.unwrap_or(wire_seq);
update_highest_received(session, wire_seq);
session.completed.insert(wire_seq, pending.logical_seq);
session.completed.insert(wire_seq);
let mut events = Vec::new();
if is_late {
events.push(ClientEvent::LateReply {
seq: wire_seq,
logical_seq: Some(pending.logical_seq),
highest_seen,
remote: self.remote,
sent_at: Some(pending.sent_at),
@@ -472,7 +478,6 @@ impl Client {
} else {
events.push(ClientEvent::EchoReply {
seq: wire_seq,
logical_seq: pending.logical_seq,
remote: self.remote,
sent_at: pending.sent_at,
received_at: now,
@@ -503,11 +508,10 @@ impl Client {
let received_stats = build_received_stats(&reply);
let highest_seen = session.highest_received_seq.unwrap_or(wire_seq);
update_highest_received(session, wire_seq);
session.completed.insert(wire_seq, timed_out.logical_seq);
session.completed.insert(wire_seq);
let mut events = vec![ClientEvent::LateReply {
seq: wire_seq,
logical_seq: Some(timed_out.logical_seq),
highest_seen,
remote: self.remote,
sent_at: Some(timed_out.sent_at),
@@ -529,7 +533,6 @@ impl Client {
{
Ok(vec![ClientEvent::LateReply {
seq: wire_seq,
logical_seq: None,
highest_seen: session.highest_received_seq.unwrap(),
remote: self.remote,
sent_at: None,
@@ -547,6 +550,7 @@ impl Client {
message: format!(
"dropped reply with untracked seq {wire_seq} (no pending or completed entry)"
),
at: now,
}])
}
}
@@ -601,15 +605,13 @@ impl Client {
reply: OpenReply,
now: ClientTimestamp,
) -> Result<OpenOutcome, ClientError> {
validate_negotiated_params(
let token = reply.token;
let negotiated = negotiate_params(
&self.requested,
&reply.params,
reply.params,
self.config.negotiation_policy,
)?;
let negotiated = NegotiatedParams {
params: reply.params.clone(),
};
let recv_buffer_size = recv_buffer_size(self.config.hmac_key.is_some(), Some(&negotiated));
let recv_buffer_size = recv_buffer_size(self.config.hmac_key.is_some(), Some(&negotiated))?;
let negotiated_dscp =
u8::try_from(negotiated.params.dscp).map_err(|_| ClientError::InvalidConfig {
reason: "negotiated dscp must be in range 0..=63".to_owned(),
@@ -617,7 +619,7 @@ impl Client {
apply_dscp_to_socket(&self.socket, self.remote, negotiated_dscp)?;
self.negotiated = Some(negotiated.clone());
self.recv_buffer.resize(recv_buffer_size, 0);
self.phase = ClientPhase::Open { token: reply.token };
self.phase = ClientPhase::Open { token };
let end_mono = if negotiated.params.duration_ns > 0 {
Some(negotiated_end_mono(
@@ -630,7 +632,6 @@ impl Client {
self.session = Some(ActiveSession {
next_wire_seq: 0,
next_logical_seq: 0,
highest_received_seq: None,
packets_sent: 0,
start_mono: now.mono,
@@ -644,14 +645,14 @@ impl Client {
let event = ClientEvent::SessionStarted {
remote: self.remote,
token: reply.token,
token,
negotiated: negotiated.clone(),
at: now,
};
Ok(OpenOutcome::Started {
remote: self.remote,
token: reply.token,
token,
negotiated,
event,
})
@@ -662,14 +663,11 @@ impl Client {
reply: OpenReply,
now: ClientTimestamp,
) -> Result<OpenOutcome, ClientError> {
validate_negotiated_params(
let negotiated = negotiate_params(
&self.requested,
&reply.params,
reply.params,
self.config.negotiation_policy,
)?;
let negotiated = NegotiatedParams {
params: reply.params.clone(),
};
self.negotiated = Some(negotiated.clone());
self.phase = ClientPhase::NoTestCompleted;
let event = ClientEvent::NoTestCompleted {
@@ -733,13 +731,16 @@ fn instant_abs_diff(left: Instant, right: Instant) -> Duration {
.unwrap_or(Duration::ZERO)
}
fn recv_buffer_size(has_hmac: bool, negotiated: Option<&NegotiatedParams>) -> usize {
match negotiated {
Some(negotiated) => echo_packet_len(has_hmac, &negotiated.params)
pub(crate) fn recv_buffer_size(
has_hmac: bool,
negotiated: Option<&NegotiatedParams>,
) -> Result<usize, ClientError> {
Ok(match negotiated {
Some(negotiated) => echo_packet_len(has_hmac, &negotiated.params)?
.saturating_add(1)
.max(MIN_RECV_BUFFER_SIZE),
None => MIN_RECV_BUFFER_SIZE,
}
})
}
fn compute_rtt(
+12 -15
View File
@@ -11,12 +11,9 @@ fn open_fails_after_close() {
let _ = recv_request(&socket, &tx);
});
let mut client = Client::connect(default_test_config(server.addr)).unwrap();
assert_open_started(client.open(ClientTimestamp::now()).unwrap());
client.close(ClientTimestamp::now()).unwrap();
assert!(matches!(
client.open(ClientTimestamp::now()),
Err(ClientError::AlreadyClosed)
));
assert_open_started(client.open().unwrap());
client.close().unwrap();
assert!(matches!(client.open(), Err(ClientError::AlreadyClosed)));
server.join();
}
@@ -31,8 +28,8 @@ fn close_sends_one_close_packet_with_negotiated_token() {
let _ = recv_request(&socket, &tx);
});
let mut client = Client::connect(default_test_config(server.addr)).unwrap();
assert_open_started(client.open(ClientTimestamp::now()).unwrap());
let events = client.close(ClientTimestamp::now()).unwrap();
assert_open_started(client.open().unwrap());
let events = client.close().unwrap();
assert_eq!(events.len(), 1);
let packets: Vec<_> = server.rx.iter().take(2).collect();
let close = &packets[1];
@@ -62,8 +59,8 @@ fn send_probe_fails_after_close() {
}
});
let mut client = Client::connect(default_test_config(server.addr)).unwrap();
assert_open_started(client.open(ClientTimestamp::now()).unwrap());
client.close(ClientTimestamp::now()).unwrap();
assert_open_started(client.open().unwrap());
client.close().unwrap();
assert!(matches!(
client.send_probe(),
Err(ClientError::AlreadyClosed)
@@ -84,14 +81,14 @@ fn close_clears_timed_out_metadata() {
..default_test_config(server.addr)
};
let mut client = Client::connect(config).unwrap();
assert_open_started(client.open(ClientTimestamp::now()).unwrap());
assert_open_started(client.open().unwrap());
client.send_probe().unwrap();
thread::sleep(Duration::from_millis(60));
client.poll_timeouts(ClientTimestamp::now()).unwrap();
client.poll_timeouts().unwrap();
assert_eq!(client.session.as_ref().unwrap().timed_out.len(), 1);
client.close(ClientTimestamp::now()).unwrap();
client.close().unwrap();
assert!(client.session.is_none());
server.join();
}
@@ -132,7 +129,7 @@ fn close_flagged_echo_reply_emits_reply_then_closes_without_sending_close() {
..default_test_config(server.addr)
};
let mut client = Client::connect(config).unwrap();
assert_open_started(client.open(ClientTimestamp::now()).unwrap());
assert_open_started(client.open().unwrap());
client.send_probe().unwrap();
let events = client.recv_once().unwrap();
@@ -170,6 +167,6 @@ fn normal_echo_reply_does_not_close_session() {
assert!(client.next_send_deadline().is_some());
assert!(client.send_probe().is_ok());
client.close(ClientTimestamp::now()).unwrap();
client.close().unwrap();
server.join();
}
+10 -3
View File
@@ -26,13 +26,13 @@ fn normal_open_applies_negotiated_dscp_after_open_and_close_clears_it() {
socket_traffic_class(&client.socket, client.remote).unwrap() & 0xfc,
0
);
assert_open_started(client.open(ClientTimestamp::now()).unwrap());
assert_open_started(client.open().unwrap());
assert_eq!(
socket_traffic_class(&client.socket, client.remote).unwrap() & 0xfc,
184
);
client.close(ClientTimestamp::now()).unwrap();
client.close().unwrap();
assert_eq!(
socket_traffic_class(&client.socket, client.remote).unwrap(),
0
@@ -58,8 +58,15 @@ fn normal_open_uses_negotiated_dscp_not_requested_dscp() {
config.negotiation_policy = NegotiationPolicy::Loose;
let mut client = Client::connect(config).unwrap();
let negotiated = assert_open_started(client.open(ClientTimestamp::now()).unwrap());
let negotiated = assert_open_started(client.open().unwrap());
assert_eq!(negotiated.params.dscp, 0);
assert_eq!(
negotiated.restrictions,
vec![crate::NegotiationRestriction::DscpChanged {
requested: 46,
negotiated: 0,
}]
);
assert_eq!(
socket_traffic_class(&client.socket, client.remote).unwrap(),
0
+7 -7
View File
@@ -14,7 +14,7 @@ fn hmac_open_success() {
});
config.server_addr = server.addr.to_string();
let mut client = Client::connect(config).unwrap();
assert_open_started(client.open(ClientTimestamp::now()).unwrap());
assert_open_started(client.open().unwrap());
server.join();
}
@@ -32,7 +32,7 @@ fn hmac_open_rejects_missing_hmac() {
config.server_addr = server.addr.to_string();
let mut client = Client::connect(config).unwrap();
assert!(matches!(
client.open(ClientTimestamp::now()),
client.open(),
Err(ClientError::Protocol(
irtt_proto::ProtoError::HmacPresenceMismatch
))
@@ -55,7 +55,7 @@ fn hmac_open_rejects_bad_hmac() {
config.server_addr = server.addr.to_string();
let mut client = Client::connect(config).unwrap();
assert!(matches!(
client.open(ClientTimestamp::now()),
client.open(),
Err(ClientError::Protocol(irtt_proto::ProtoError::BadHmac))
));
server.join();
@@ -76,8 +76,8 @@ fn hmac_close_packet_includes_valid_hmac() {
});
config.server_addr = server.addr.to_string();
let mut client = Client::connect(config).unwrap();
assert_open_started(client.open(ClientTimestamp::now()).unwrap());
client.close(ClientTimestamp::now()).unwrap();
assert_open_started(client.open().unwrap());
client.close().unwrap();
let packets: Vec<_> = server.rx.iter().take(2).collect();
let close = &packets[1];
assert_eq!(close[3], flags::FLAG_CLOSE | FLAG_HMAC);
@@ -125,7 +125,7 @@ fn bad_hmac_reply_is_dropped() {
..default_test_config(server.addr)
};
let mut client = Client::connect(config).unwrap();
assert_open_started(client.open(ClientTimestamp::now()).unwrap());
assert_open_started(client.open().unwrap());
client.send_probe().unwrap();
thread::sleep(Duration::from_millis(30));
let events = client.recv_once().unwrap();
@@ -174,7 +174,7 @@ fn hmac_echo_request_reply_works() {
..default_test_config(server.addr)
};
let mut client = Client::connect(config).unwrap();
assert_open_started(client.open(ClientTimestamp::now()).unwrap());
assert_open_started(client.open().unwrap());
client.send_probe().unwrap();
thread::sleep(Duration::from_millis(50));
let events = client.recv_once().unwrap();
+2 -2
View File
@@ -1,8 +1,8 @@
use super::*;
use crate::config::{NegotiationPolicy, DEFAULT_OPEN_TIMEOUTS};
use irtt_proto::{
echo_packet_len, flags::FLAG_HMAC, flags::FLAG_OPEN, flags::FLAG_REPLY, verify_hmac, Clock,
ReceivedStats, StampAt, HMAC_SIZE, MAGIC,
flags::FLAG_HMAC, flags::FLAG_OPEN, flags::FLAG_REPLY, verify_hmac, Clock, ReceivedStats,
StampAt, HMAC_SIZE, MAGIC,
};
use std::{thread, time::SystemTime};
@@ -1,32 +1,72 @@
use super::*;
use crate::NegotiationRestriction;
fn assert_negotiates(
requested: &Params,
returned: &Params,
policy: NegotiationPolicy,
) -> NegotiatedParams {
negotiate_params(requested, returned.clone(), policy)
.unwrap_or_else(|err| panic!("expected negotiation success, got {err:?}"))
}
fn rejection_reason(requested: &Params, returned: &Params, policy: NegotiationPolicy) -> String {
match negotiate_params(requested, returned.clone(), policy) {
Err(ClientError::NegotiationRejected { reason }) => reason,
other => panic!("expected negotiation rejection, got {other:?}"),
}
}
#[test]
fn strict_negotiation_accepts_identical_params() {
let config = ClientConfig::default();
let params = params_from_config(&config).unwrap();
assert!(validate_negotiated_params(&params, &params, NegotiationPolicy::Strict).is_ok());
let negotiated = assert_negotiates(&params, &params, NegotiationPolicy::Strict);
assert_eq!(negotiated.params, params);
assert!(negotiated.restrictions.is_empty());
}
#[test]
fn strict_negotiation_rejects_changed_params() {
let config = ClientConfig::default();
let config = ClientConfig {
dscp: 46,
..ClientConfig::default()
};
let requested = params_from_config(&config).unwrap();
let mut returned = requested.clone();
returned.dscp = 1;
assert!(matches!(
validate_negotiated_params(&requested, &returned, NegotiationPolicy::Strict),
Err(ClientError::NegotiationRejected { .. })
));
returned.dscp = 0;
assert_eq!(
rejection_reason(&requested, &returned, NegotiationPolicy::Strict),
NegotiationRestriction::DscpChanged {
requested: requested.dscp,
negotiated: returned.dscp,
}
.message()
);
}
#[test]
fn loose_negotiation_accepts_server_restricted_params() {
let config = ClientConfig::default();
let requested = params_from_config(&config).unwrap();
let mut requested = params_from_config(&config).unwrap();
requested.length = 128;
let mut returned = requested.clone();
returned.duration_ns /= 2;
returned.length = 0;
assert!(validate_negotiated_params(&requested, &returned, NegotiationPolicy::Loose).is_ok());
let negotiated = assert_negotiates(&requested, &returned, NegotiationPolicy::Loose);
assert_eq!(
negotiated.restrictions,
vec![
NegotiationRestriction::DurationReduced {
requested_ns: requested.duration_ns,
negotiated_ns: returned.duration_ns,
},
NegotiationRestriction::LengthReduced {
requested: requested.length,
negotiated: returned.length,
},
]
);
}
#[test]
@@ -37,11 +77,10 @@ fn loose_negotiation_rejects_non_positive_returned_interval() {
for interval_ns in [0, -1] {
let mut returned = requested.clone();
returned.interval_ns = interval_ns;
assert!(matches!(
validate_negotiated_params(&requested, &returned, NegotiationPolicy::Loose),
Err(ClientError::NegotiationRejected { reason })
if reason == "interval must be positive"
));
assert_eq!(
rejection_reason(&requested, &returned, NegotiationPolicy::Loose),
"interval must be positive"
);
}
}
@@ -51,21 +90,28 @@ fn loose_negotiation_accepts_documented_duration_combinations() {
let requested_finite = params_from_config(&finite_config).unwrap();
let returned_same_finite = requested_finite.clone();
assert!(validate_negotiated_params(
assert!(assert_negotiates(
&requested_finite,
&returned_same_finite,
NegotiationPolicy::Loose
)
.is_ok());
.restrictions
.is_empty());
let mut returned_shorter_finite = requested_finite.clone();
returned_shorter_finite.duration_ns /= 2;
assert!(validate_negotiated_params(
assert_eq!(
assert_negotiates(
&requested_finite,
&returned_shorter_finite,
NegotiationPolicy::Loose
)
.is_ok());
.restrictions,
vec![NegotiationRestriction::DurationReduced {
requested_ns: requested_finite.duration_ns,
negotiated_ns: returned_shorter_finite.duration_ns,
}]
);
let continuous_config = ClientConfig {
duration: None,
@@ -74,21 +120,28 @@ fn loose_negotiation_accepts_documented_duration_combinations() {
let requested_continuous = params_from_config(&continuous_config).unwrap();
let returned_continuous = requested_continuous.clone();
assert!(validate_negotiated_params(
assert!(assert_negotiates(
&requested_continuous,
&returned_continuous,
NegotiationPolicy::Loose
)
.is_ok());
.restrictions
.is_empty());
let mut returned_finite = requested_continuous.clone();
returned_finite.duration_ns = 1_000_000_000;
assert!(validate_negotiated_params(
assert_eq!(
assert_negotiates(
&requested_continuous,
&returned_finite,
NegotiationPolicy::Loose
)
.is_ok());
.restrictions,
vec![NegotiationRestriction::DurationReduced {
requested_ns: requested_continuous.duration_ns,
negotiated_ns: returned_finite.duration_ns,
}]
);
}
#[test]
@@ -98,11 +151,10 @@ fn loose_negotiation_rejects_finite_request_returned_continuous() {
let mut returned = requested.clone();
returned.duration_ns = 0;
assert!(matches!(
validate_negotiated_params(&requested, &returned, NegotiationPolicy::Loose),
Err(ClientError::NegotiationRejected { reason })
if reason == "server returned continuous duration for finite request"
));
assert_eq!(
rejection_reason(&requested, &returned, NegotiationPolicy::Loose),
"server returned continuous duration for finite request"
);
}
#[test]
@@ -111,10 +163,10 @@ fn loose_negotiation_rejects_negative_returned_length() {
let requested = params_from_config(&config).unwrap();
let mut returned = requested.clone();
returned.length = -1;
assert!(matches!(
validate_negotiated_params(&requested, &returned, NegotiationPolicy::Loose),
Err(ClientError::NegotiationRejected { reason }) if reason == "length must be non-negative"
));
assert_eq!(
rejection_reason(&requested, &returned, NegotiationPolicy::Loose),
"length must be non-negative"
);
}
#[test]
@@ -123,10 +175,10 @@ fn strict_negotiation_rejects_negative_returned_length() {
let requested = params_from_config(&config).unwrap();
let mut returned = requested.clone();
returned.length = -1;
assert!(matches!(
validate_negotiated_params(&requested, &returned, NegotiationPolicy::Strict),
Err(ClientError::NegotiationRejected { reason }) if reason == "length must be non-negative"
));
assert_eq!(
rejection_reason(&requested, &returned, NegotiationPolicy::Strict),
"length must be non-negative"
);
}
#[test]
@@ -137,10 +189,80 @@ fn loose_negotiation_rejects_runtime_invalid_returned_dscp() {
for dscp in [-1, 64] {
let mut returned = requested.clone();
returned.dscp = dscp;
assert!(matches!(
validate_negotiated_params(&requested, &returned, NegotiationPolicy::Loose),
Err(ClientError::NegotiationRejected { reason })
if reason == "dscp must be in range 0..=63"
));
assert_eq!(
rejection_reason(&requested, &returned, NegotiationPolicy::Loose),
"dscp must be in range 0..=63"
);
}
}
#[test]
fn loose_negotiation_records_dscp_disabled_by_server() {
let config = ClientConfig {
dscp: 46,
..ClientConfig::default()
};
let requested = params_from_config(&config).unwrap();
let mut returned = requested.clone();
returned.dscp = 0;
let negotiated = assert_negotiates(&requested, &returned, NegotiationPolicy::Loose);
assert_eq!(
negotiated.restrictions,
vec![NegotiationRestriction::DscpChanged {
requested: 46,
negotiated: 0,
}]
);
}
#[test]
fn strict_negotiation_rejects_dscp_disabled_by_server_as_specific_restriction() {
let config = ClientConfig {
dscp: 46,
..ClientConfig::default()
};
let requested = params_from_config(&config).unwrap();
let mut returned = requested.clone();
returned.dscp = 0;
assert_eq!(
rejection_reason(&requested, &returned, NegotiationPolicy::Strict),
NegotiationRestriction::DscpChanged {
requested: 46,
negotiated: 0,
}
.message()
);
}
#[test]
fn negotiation_rejects_unsupported_dscp_changes() {
let config = ClientConfig {
dscp: 46,
..ClientConfig::default()
};
let requested = params_from_config(&config).unwrap();
let mut returned = requested.clone();
returned.dscp = 8;
assert_eq!(
rejection_reason(&requested, &returned, NegotiationPolicy::Loose),
"server returned unsupported DSCP change"
);
assert_eq!(
rejection_reason(&requested, &returned, NegotiationPolicy::Strict),
"server returned unsupported DSCP change"
);
let zero_config = ClientConfig::default();
let zero_requested = params_from_config(&zero_config).unwrap();
let mut returned = zero_requested.clone();
returned.dscp = 46;
assert_eq!(
rejection_reason(&zero_requested, &returned, NegotiationPolicy::Loose),
"server returned unsupported DSCP change"
);
}
+19 -17
View File
@@ -8,13 +8,10 @@ fn no_test_open_close_succeeds_on_open_reply_close() {
let server = no_test_server(params.clone(), 0);
config.server_addr = server.addr.to_string();
let mut client = Client::connect(config).unwrap();
let negotiated = assert_no_test_completed(client.open(ClientTimestamp::now()).unwrap());
let negotiated = assert_no_test_completed(client.open().unwrap());
assert_eq!(negotiated.params, params);
assert_eq!(client.negotiated.as_ref(), Some(&negotiated));
assert!(matches!(
client.close(ClientTimestamp::now()),
Err(ClientError::NotOpen)
));
assert!(matches!(client.close(), Err(ClientError::NotOpen)));
server.join();
}
@@ -26,7 +23,7 @@ fn no_test_success_validates_params() {
let server = no_test_server(params.clone(), 0);
config.server_addr = server.addr.to_string();
let mut client = Client::connect(config).unwrap();
let negotiated = assert_no_test_completed(client.open(ClientTimestamp::now()).unwrap());
let negotiated = assert_no_test_completed(client.open().unwrap());
assert_eq!(negotiated.params, params);
server.join();
}
@@ -40,7 +37,7 @@ fn no_test_rejects_non_close_open_reply() {
config.server_addr = server.addr.to_string();
let mut client = Client::connect(config).unwrap();
assert!(matches!(
client.open(ClientTimestamp::now()),
client.open(),
Err(ClientError::UnexpectedNoTestReply)
));
server.join();
@@ -55,7 +52,7 @@ fn no_test_rejects_non_zero_token_with_close_reply() {
config.server_addr = server.addr.to_string();
let mut client = Client::connect(config).unwrap();
assert!(matches!(
client.open(ClientTimestamp::now()),
client.open(),
Err(ClientError::NonZeroNoTestToken { token: TOKEN })
));
server.join();
@@ -71,7 +68,7 @@ fn no_test_strict_negotiation_rejects_changed_params() {
config.server_addr = server.addr.to_string();
let mut client = Client::connect(config).unwrap();
assert!(matches!(
client.open(ClientTimestamp::now()),
client.open(),
Err(ClientError::NegotiationRejected { .. })
));
server.join();
@@ -82,13 +79,21 @@ fn no_test_loose_negotiation_accepts_restricted_params() {
let mut config = default_test_config(SocketAddr::from(([127, 0, 0, 1], 1)));
config.run_mode = RunMode::NoTest;
config.negotiation_policy = NegotiationPolicy::Loose;
let mut params = params_from_config(&config).unwrap();
let requested = params_from_config(&config).unwrap();
let mut params = requested.clone();
params.duration_ns /= 2;
let server = no_test_server(params.clone(), 0);
config.server_addr = server.addr.to_string();
let mut client = Client::connect(config).unwrap();
let negotiated = assert_no_test_completed(client.open(ClientTimestamp::now()).unwrap());
let negotiated = assert_no_test_completed(client.open().unwrap());
assert_eq!(negotiated.params, params);
assert_eq!(
negotiated.restrictions,
vec![crate::NegotiationRestriction::DurationReduced {
requested_ns: requested.duration_ns,
negotiated_ns: params.duration_ns,
}]
);
server.join();
}
@@ -100,7 +105,7 @@ fn send_probe_fails_after_no_test_completed() {
let server = no_test_server(params, 0);
config.server_addr = server.addr.to_string();
let mut client = Client::connect(config).unwrap();
assert_no_test_completed(client.open(ClientTimestamp::now()).unwrap());
assert_no_test_completed(client.open().unwrap());
assert!(matches!(
client.send_probe(),
Err(ClientError::AlreadyCompleted)
@@ -116,10 +121,7 @@ fn open_fails_after_no_test_completed() {
let server = no_test_server(params, 0);
config.server_addr = server.addr.to_string();
let mut client = Client::connect(config).unwrap();
assert_no_test_completed(client.open(ClientTimestamp::now()).unwrap());
assert!(matches!(
client.open(ClientTimestamp::now()),
Err(ClientError::AlreadyCompleted)
));
assert_no_test_completed(client.open().unwrap());
assert!(matches!(client.open(), Err(ClientError::AlreadyCompleted)));
server.join();
}
+8 -20
View File
@@ -18,7 +18,7 @@ fn successful_open_handshake() {
let server = open_success_server(params.clone());
let mut client = Client::connect(default_test_config(server.addr)).unwrap();
let negotiated = assert_open_started(client.open(ClientTimestamp::now()).unwrap());
let negotiated = assert_open_started(client.open().unwrap());
assert_eq!(negotiated.params, params);
assert!(matches!(client.phase, ClientPhase::Open { token: TOKEN }));
server.join();
@@ -30,11 +30,8 @@ fn open_fails_when_already_open() {
let params = params_from_config(&config).unwrap();
let server = open_success_server(params);
let mut client = Client::connect(default_test_config(server.addr)).unwrap();
assert_open_started(client.open(ClientTimestamp::now()).unwrap());
assert!(matches!(
client.open(ClientTimestamp::now()),
Err(ClientError::AlreadyOpen)
));
assert_open_started(client.open().unwrap());
assert!(matches!(client.open(), Err(ClientError::AlreadyOpen)));
server.join();
}
@@ -53,7 +50,7 @@ fn open_retries_after_first_timeout() {
..default_test_config(server.addr)
};
let mut client = Client::connect(config).unwrap();
let outcome = client.open(ClientTimestamp::now()).unwrap();
let outcome = client.open().unwrap();
assert_open_started(outcome);
assert_eq!(server.rx.iter().take(2).count(), 2);
server.join();
@@ -67,10 +64,7 @@ fn open_timeout_after_all_timeouts() {
..default_test_config(server.addr)
};
let mut client = Client::connect(config).unwrap();
assert!(matches!(
client.open(ClientTimestamp::now()),
Err(ClientError::OpenTimeout)
));
assert!(matches!(client.open(), Err(ClientError::OpenTimeout)));
assert_eq!(server.rx.iter().take(2).count(), 2);
server.join();
}
@@ -87,10 +81,7 @@ fn open_restores_configured_read_timeout_after_timeout() {
..default_test_config(server.addr)
};
let mut client = Client::connect(config).unwrap();
assert!(matches!(
client.open(ClientTimestamp::now()),
Err(ClientError::OpenTimeout)
));
assert!(matches!(client.open(), Err(ClientError::OpenTimeout)));
let start = std::time::Instant::now();
let mut buf = [0_u8; 1];
@@ -108,7 +99,7 @@ fn protocol_version_mismatch_fails() {
let server = open_success_server(params);
let mut client = Client::connect(default_test_config(server.addr)).unwrap();
assert!(matches!(
client.open(ClientTimestamp::now()),
client.open(),
Err(ClientError::ProtocolVersionMismatch { received: 2, .. })
));
server.join();
@@ -124,9 +115,6 @@ fn server_rejection_fails_in_normal_mode() {
socket.send_to(&reply, peer).unwrap();
});
let mut client = Client::connect(default_test_config(server.addr)).unwrap();
assert!(matches!(
client.open(ClientTimestamp::now()),
Err(ClientError::ServerRejected)
));
assert!(matches!(client.open(), Err(ClientError::ServerRejected)));
server.join();
}
@@ -1,4 +1,5 @@
use super::*;
use support::echo_packet_len;
#[cfg(not(all(target_os = "linux", feature = "ancillary")))]
fn assert_packet_meta_unavailable(packet_meta: &crate::event::PacketMeta) {
@@ -38,19 +39,14 @@ fn send_probe_sends_valid_echo_request() {
..default_test_config(server.addr)
};
let mut client = Client::connect(config).unwrap();
assert_open_started(client.open(ClientTimestamp::now()).unwrap());
assert_open_started(client.open().unwrap());
let events = client.send_probe().unwrap();
assert_eq!(events.len(), 1);
match &events[0] {
ClientEvent::EchoSent {
seq,
logical_seq,
remote,
bytes,
..
seq, remote, bytes, ..
} => {
assert_eq!(*seq, 0);
assert_eq!(*logical_seq, 0);
assert_eq!(*remote, server.addr);
assert_eq!(*bytes, echo_packet_len(false, &params));
}
@@ -69,7 +65,7 @@ fn send_probe_sends_valid_echo_request() {
assert_eq!(req_token, TOKEN);
let seq = u32::from_le_bytes(echo_req[12..16].try_into().unwrap());
assert_eq!(seq, 0);
client.close(ClientTimestamp::now()).unwrap();
client.close().unwrap();
server.join();
}
@@ -89,7 +85,7 @@ fn echo_sent_reports_schedule_and_timer_error() {
mono: Instant::now(),
wall: SystemTime::now(),
};
assert_open_started(client.open(start).unwrap());
assert_open_started(client.open().unwrap());
let session_start = client.session.as_ref().unwrap().start_mono;
assert!(
session_start >= start.mono,
@@ -115,7 +111,7 @@ fn echo_sent_reports_schedule_and_timer_error() {
other => panic!("expected EchoSent, got {other:?}"),
}
client.close(ClientTimestamp::now()).unwrap();
client.close().unwrap();
server.join();
}
@@ -131,7 +127,7 @@ fn send_probe_starts_seq_at_zero_and_increments() {
..default_test_config(server.addr)
};
let mut client = Client::connect(config).unwrap();
assert_open_started(client.open(ClientTimestamp::now()).unwrap());
assert_open_started(client.open().unwrap());
client.send_probe().unwrap();
client.send_probe().unwrap();
client.send_probe().unwrap();
@@ -146,7 +142,7 @@ fn send_probe_starts_seq_at_zero_and_increments() {
let seq = u32::from_le_bytes(pkt[12..16].try_into().unwrap());
assert_eq!(seq, i as u32);
}
client.close(ClientTimestamp::now()).unwrap();
client.close().unwrap();
server.join();
}
@@ -172,7 +168,7 @@ fn send_probe_respects_finite_duration_exclusive_end() {
..default_test_config(server.addr)
};
let mut client = Client::connect(config).unwrap();
assert_open_started(client.open(ClientTimestamp::now()).unwrap());
assert_open_started(client.open().unwrap());
let session = client.session.as_ref().unwrap();
let start = session.start_mono;
@@ -199,7 +195,7 @@ fn send_probe_respects_finite_duration_exclusive_end() {
assert!(client.session.as_ref().unwrap().sending_done);
assert!(client.next_send_deadline().is_none());
client.close(ClientTimestamp::now()).unwrap();
client.close().unwrap();
server.join();
}
@@ -225,7 +221,7 @@ fn continuous_duration_keeps_generating_send_deadlines() {
..default_test_config(server.addr)
};
let mut client = Client::connect(config).unwrap();
assert_open_started(client.open(ClientTimestamp::now()).unwrap());
assert_open_started(client.open().unwrap());
let start = client.session.as_ref().unwrap().start_mono;
let interval = Duration::from_millis(500);
@@ -240,7 +236,7 @@ fn continuous_duration_keeps_generating_send_deadlines() {
assert!(!client.session.as_ref().unwrap().sending_done);
}
client.close(ClientTimestamp::now()).unwrap();
client.close().unwrap();
server.join();
}
@@ -262,7 +258,7 @@ fn next_send_deadline_advances_by_interval() {
let deadline2 = client.next_send_deadline().unwrap();
assert_eq!(deadline2, start + Duration::from_secs(2));
client.close(ClientTimestamp::now()).unwrap();
client.close().unwrap();
server.join();
}
@@ -301,7 +297,7 @@ fn send_probe_reports_schedule_overflow() {
..default_test_config(server.addr)
};
let mut client = Client::connect(config).unwrap();
assert_open_started(client.open(ClientTimestamp::now()).unwrap());
assert_open_started(client.open().unwrap());
client.session.as_mut().unwrap().packets_sent = u64::MAX - 1;
assert!(matches!(
@@ -311,30 +307,6 @@ fn send_probe_reports_schedule_overflow() {
server.join();
}
#[test]
fn send_probe_reports_logical_sequence_counter_overflow() {
let params = default_params();
let server = silent_open_server(params);
let config = ClientConfig {
socket_config: crate::SocketConfig {
recv_timeout: Some(Duration::from_millis(200)),
..Default::default()
},
..default_test_config(server.addr)
};
let mut client = Client::connect(config).unwrap();
assert_open_started(client.open(ClientTimestamp::now()).unwrap());
client.session.as_mut().unwrap().next_logical_seq = u64::MAX;
assert!(matches!(
client.send_probe(),
Err(ClientError::CounterOverflow {
counter: "next_logical_seq"
})
));
server.join();
}
#[test]
fn send_probe_reports_packets_sent_counter_overflow() {
let params = default_params();
@@ -347,7 +319,7 @@ fn send_probe_reports_packets_sent_counter_overflow() {
..default_test_config(server.addr)
};
let mut client = Client::connect(config).unwrap();
assert_open_started(client.open(ClientTimestamp::now()).unwrap());
assert_open_started(client.open().unwrap());
client.session.as_mut().unwrap().packets_sent = u64::MAX;
assert!(matches!(
@@ -371,7 +343,7 @@ fn recv_once_returns_empty_on_timeout() {
..default_test_config(server.addr)
};
let mut client = Client::connect(config).unwrap();
assert_open_started(client.open(ClientTimestamp::now()).unwrap());
assert_open_started(client.open().unwrap());
let events = client.recv_once().unwrap();
assert!(events.is_empty());
server.join();
@@ -390,7 +362,6 @@ fn recv_once_decodes_echo_reply_and_emits_event() {
match &events[0] {
ClientEvent::EchoReply {
seq,
logical_seq,
rtt,
received_stats,
server_timing,
@@ -399,7 +370,6 @@ fn recv_once_decodes_echo_reply_and_emits_event() {
..
} => {
assert_eq!(*seq, 0);
assert_eq!(*logical_seq, 0);
assert_eq!(*bytes, echo_packet_len(false, &params));
assert!(rtt.raw > Duration::ZERO);
assert_eq!(rtt.effective, rtt.adjusted.unwrap_or(rtt.raw));
@@ -431,7 +401,7 @@ fn recv_once_decodes_echo_reply_and_emits_event() {
}
other => panic!("expected EchoReply, got {other:?}"),
}
client.close(ClientTimestamp::now()).unwrap();
client.close().unwrap();
server.join();
}
@@ -477,7 +447,7 @@ fn recv_once_decodes_only_received_bytes_after_longer_datagram() {
..default_test_config(server.addr)
};
let mut client = Client::connect(config).unwrap();
assert_open_started(client.open(ClientTimestamp::now()).unwrap());
assert_open_started(client.open().unwrap());
client.send_probe().unwrap();
client.send_probe().unwrap();
@@ -487,14 +457,8 @@ fn recv_once_decodes_only_received_bytes_after_longer_datagram() {
let events = client.recv_once().unwrap();
assert_eq!(events.len(), 1);
match &events[0] {
ClientEvent::EchoReply {
seq,
logical_seq,
bytes,
..
} => {
ClientEvent::EchoReply { seq, bytes, .. } => {
assert_eq!(*seq, 1);
assert_eq!(*logical_seq, 1);
assert_eq!(*bytes, echo_packet_len(false, &params));
}
other => panic!("expected EchoReply after shorter datagram, got {other:?}"),
@@ -520,7 +484,7 @@ fn echo_reply_metadata_is_unavailable_without_ancillary() {
other => panic!("expected EchoReply, got {other:?}"),
}
client.close(ClientTimestamp::now()).unwrap();
client.close().unwrap();
server.join();
}
@@ -550,7 +514,7 @@ fn echo_reply_metadata_propagates_observed_dscp_with_ancillary() {
..default_test_config(server.addr)
};
let mut client = Client::connect(config).unwrap();
assert_open_started(client.open(ClientTimestamp::now()).unwrap());
assert_open_started(client.open().unwrap());
client.send_probe().unwrap();
thread::sleep(Duration::from_millis(50));
@@ -563,7 +527,7 @@ fn echo_reply_metadata_propagates_observed_dscp_with_ancillary() {
metadata_unavailable_skip(
"echo_reply_metadata_propagates_observed_dscp_with_ancillary",
);
client.close(ClientTimestamp::now()).unwrap();
client.close().unwrap();
server.join();
return;
};
@@ -574,7 +538,7 @@ fn echo_reply_metadata_propagates_observed_dscp_with_ancillary() {
other => panic!("expected EchoReply, got {other:?}"),
}
client.close(ClientTimestamp::now()).unwrap();
client.close().unwrap();
server.join();
}
@@ -595,7 +559,7 @@ fn echo_reply_metadata_preserves_observed_zero_with_ancillary() {
metadata_unavailable_skip(
"echo_reply_metadata_preserves_observed_zero_with_ancillary",
);
client.close(ClientTimestamp::now()).unwrap();
client.close().unwrap();
server.join();
return;
};
@@ -606,7 +570,7 @@ fn echo_reply_metadata_preserves_observed_zero_with_ancillary() {
other => panic!("expected EchoReply, got {other:?}"),
}
client.close(ClientTimestamp::now()).unwrap();
client.close().unwrap();
server.join();
}
@@ -627,7 +591,7 @@ fn echo_reply_metadata_propagates_kernel_rx_timestamp_with_ancillary() {
kernel_rx_timestamp_unavailable_skip(
"echo_reply_metadata_propagates_kernel_rx_timestamp_with_ancillary",
);
client.close(ClientTimestamp::now()).unwrap();
client.close().unwrap();
server.join();
return;
};
@@ -636,7 +600,7 @@ fn echo_reply_metadata_propagates_kernel_rx_timestamp_with_ancillary() {
other => panic!("expected EchoReply, got {other:?}"),
}
client.close(ClientTimestamp::now()).unwrap();
client.close().unwrap();
server.join();
}
@@ -654,7 +618,7 @@ fn echo_reply_rtt_uses_client_monotonic() {
} else {
panic!("expected EchoReply");
}
client.close(ClientTimestamp::now()).unwrap();
client.close().unwrap();
server.join();
}
@@ -679,7 +643,7 @@ fn server_processing_subtracted_when_valid() {
} else {
panic!("expected EchoReply");
}
client.close(ClientTimestamp::now()).unwrap();
client.close().unwrap();
server.join();
}
@@ -724,7 +688,7 @@ fn received_stats_parsed_into_sample() {
} else {
panic!("expected EchoReply");
}
client.close(ClientTimestamp::now()).unwrap();
client.close().unwrap();
server.join();
}
@@ -757,7 +721,7 @@ fn wrong_token_reply_is_dropped() {
..default_test_config(server.addr)
};
let mut client = Client::connect(config).unwrap();
assert_open_started(client.open(ClientTimestamp::now()).unwrap());
assert_open_started(client.open().unwrap());
client.send_probe().unwrap();
thread::sleep(Duration::from_millis(30));
let events = client.recv_once().unwrap();
@@ -796,7 +760,7 @@ fn duplicate_reply_emits_duplicate_event() {
..default_test_config(server.addr)
};
let mut client = Client::connect(config).unwrap();
assert_open_started(client.open(ClientTimestamp::now()).unwrap());
assert_open_started(client.open().unwrap());
client.send_probe().unwrap();
thread::sleep(Duration::from_millis(50));
@@ -850,7 +814,7 @@ fn out_of_order_reply_emits_late_event() {
..default_test_config(server.addr)
};
let mut client = Client::connect(config).unwrap();
assert_open_started(client.open(ClientTimestamp::now()).unwrap());
assert_open_started(client.open().unwrap());
client.send_probe().unwrap();
client.send_probe().unwrap();
thread::sleep(Duration::from_millis(50));
@@ -891,16 +855,16 @@ fn poll_timeouts_emits_echo_loss() {
..default_test_config(server.addr)
};
let mut client = Client::connect(config).unwrap();
assert_open_started(client.open(ClientTimestamp::now()).unwrap());
assert_open_started(client.open().unwrap());
client.send_probe().unwrap();
client.send_probe().unwrap();
let no_loss = client.poll_timeouts(ClientTimestamp::now()).unwrap();
let no_loss = client.poll_timeouts().unwrap();
assert!(no_loss.is_empty());
thread::sleep(Duration::from_millis(150));
let events = client.poll_timeouts(ClientTimestamp::now()).unwrap();
let events = client.poll_timeouts().unwrap();
assert_eq!(events.len(), 2);
for event in &events {
assert!(matches!(event, ClientEvent::EchoLoss { .. }));
@@ -921,11 +885,11 @@ fn poll_timeouts_removes_expired_pending() {
..default_test_config(server.addr)
};
let mut client = Client::connect(config).unwrap();
assert_open_started(client.open(ClientTimestamp::now()).unwrap());
assert_open_started(client.open().unwrap());
client.send_probe().unwrap();
thread::sleep(Duration::from_millis(150));
client.poll_timeouts(ClientTimestamp::now()).unwrap();
client.poll_timeouts().unwrap();
let session = client.session.as_ref().unwrap();
assert_eq!(session.pending.len(), 0);
@@ -967,11 +931,11 @@ fn late_reply_after_timeout_preserves_measurement_metadata() {
..default_test_config(server.addr)
};
let mut client = Client::connect(config).unwrap();
assert_open_started(client.open(ClientTimestamp::now()).unwrap());
assert_open_started(client.open().unwrap());
client.send_probe().unwrap();
thread::sleep(Duration::from_millis(60));
let losses = client.poll_timeouts(ClientTimestamp::now()).unwrap();
let losses = client.poll_timeouts().unwrap();
assert!(matches!(&losses[0], ClientEvent::EchoLoss { seq: 0, .. }));
assert_eq!(client.session.as_ref().unwrap().pending.len(), 0);
assert_eq!(client.session.as_ref().unwrap().timed_out.len(), 1);
@@ -980,7 +944,6 @@ fn late_reply_after_timeout_preserves_measurement_metadata() {
match &late[0] {
ClientEvent::LateReply {
seq,
logical_seq,
sent_at,
rtt,
server_timing,
@@ -990,7 +953,6 @@ fn late_reply_after_timeout_preserves_measurement_metadata() {
..
} => {
assert_eq!(*seq, 0);
assert_eq!(*logical_seq, Some(0));
assert!(sent_at.is_some());
assert!(rtt.is_some());
assert!(server_timing.is_some());
@@ -1014,7 +976,7 @@ fn late_reply_after_timeout_preserves_measurement_metadata() {
} if *bytes == echo_packet_len(false, &default_params())
));
client.close(ClientTimestamp::now()).unwrap();
client.close().unwrap();
server.join();
}
@@ -1046,11 +1008,11 @@ fn late_reply_metadata_is_unavailable_without_ancillary() {
..default_test_config(server.addr)
};
let mut client = Client::connect(config).unwrap();
assert_open_started(client.open(ClientTimestamp::now()).unwrap());
assert_open_started(client.open().unwrap());
client.send_probe().unwrap();
thread::sleep(Duration::from_millis(60));
let losses = client.poll_timeouts(ClientTimestamp::now()).unwrap();
let losses = client.poll_timeouts().unwrap();
assert!(matches!(&losses[0], ClientEvent::EchoLoss { seq: 0, .. }));
let late = client.recv_once().unwrap();
@@ -1062,7 +1024,7 @@ fn late_reply_metadata_is_unavailable_without_ancillary() {
other => panic!("expected LateReply, got {other:?}"),
}
client.close(ClientTimestamp::now()).unwrap();
client.close().unwrap();
server.join();
}
@@ -1095,11 +1057,11 @@ fn late_reply_metadata_propagates_observed_dscp_with_ancillary() {
..default_test_config(server.addr)
};
let mut client = Client::connect(config).unwrap();
assert_open_started(client.open(ClientTimestamp::now()).unwrap());
assert_open_started(client.open().unwrap());
client.send_probe().unwrap();
thread::sleep(Duration::from_millis(60));
let losses = client.poll_timeouts(ClientTimestamp::now()).unwrap();
let losses = client.poll_timeouts().unwrap();
assert!(matches!(&losses[0], ClientEvent::EchoLoss { seq: 0, .. }));
let events = client.recv_once().unwrap();
@@ -1110,7 +1072,7 @@ fn late_reply_metadata_propagates_observed_dscp_with_ancillary() {
metadata_unavailable_skip(
"late_reply_metadata_propagates_observed_dscp_with_ancillary",
);
client.close(ClientTimestamp::now()).unwrap();
client.close().unwrap();
server.join();
return;
};
@@ -1128,7 +1090,7 @@ fn late_reply_metadata_propagates_observed_dscp_with_ancillary() {
other => panic!("expected LateReply, got {other:?}"),
}
client.close(ClientTimestamp::now()).unwrap();
client.close().unwrap();
server.join();
}
@@ -1149,7 +1111,7 @@ fn pending_map_bounded() {
..default_test_config(server.addr)
};
let mut client = Client::connect(config).unwrap();
assert_open_started(client.open(ClientTimestamp::now()).unwrap());
assert_open_started(client.open().unwrap());
client.send_probe().unwrap();
client.send_probe().unwrap();
client.send_probe().unwrap();
@@ -1157,7 +1119,7 @@ fn pending_map_bounded() {
client.send_probe(),
Err(ClientError::PendingLimitExceeded { limit: 3 })
));
client.close(ClientTimestamp::now()).unwrap();
client.close().unwrap();
server.join();
}
@@ -1199,7 +1161,7 @@ fn minimal_negotiated_layout_works() {
..default_test_config(server.addr)
};
let mut client = Client::connect(config).unwrap();
assert_open_started(client.open(ClientTimestamp::now()).unwrap());
assert_open_started(client.open().unwrap());
client.send_probe().unwrap();
thread::sleep(Duration::from_millis(30));
let events = client.recv_once().unwrap();
@@ -1253,7 +1215,7 @@ fn late_reply_with_pending_preserves_rtt() {
..default_test_config(server.addr)
};
let mut client = Client::connect(config).unwrap();
assert_open_started(client.open(ClientTimestamp::now()).unwrap());
assert_open_started(client.open().unwrap());
client.send_probe().unwrap();
client.send_probe().unwrap();
client.send_probe().unwrap();
@@ -1296,7 +1258,7 @@ fn pending_full_does_not_send_packet() {
..default_test_config(server.addr)
};
let mut client = Client::connect(config).unwrap();
assert_open_started(client.open(ClientTimestamp::now()).unwrap());
assert_open_started(client.open().unwrap());
client.send_probe().unwrap();
client.send_probe().unwrap();
@@ -1326,7 +1288,7 @@ fn pending_full_does_not_send_packet() {
"no packet should be sent when pending is full"
);
client.close(ClientTimestamp::now()).unwrap();
client.close().unwrap();
server.join();
}
@@ -1361,7 +1323,7 @@ fn unmatched_future_reply_emits_warning_not_late() {
..default_test_config(server.addr)
};
let mut client = Client::connect(config).unwrap();
assert_open_started(client.open(ClientTimestamp::now()).unwrap());
assert_open_started(client.open().unwrap());
client.send_probe().unwrap();
thread::sleep(Duration::from_millis(50));
@@ -1419,7 +1381,7 @@ fn unmatched_future_reply_does_not_update_highest_received_seq() {
..default_test_config(server.addr)
};
let mut client = Client::connect(config).unwrap();
assert_open_started(client.open(ClientTimestamp::now()).unwrap());
assert_open_started(client.open().unwrap());
client.send_probe().unwrap();
client.send_probe().unwrap();
thread::sleep(Duration::from_millis(50));
@@ -1523,7 +1485,7 @@ fn matched_reply_with_reversed_monotonic_time_still_emits_event() {
..default_test_config(server.addr)
};
let mut client = Client::connect(config).unwrap();
assert_open_started(client.open(ClientTimestamp::now()).unwrap());
assert_open_started(client.open().unwrap());
let base = Instant::now() + Duration::from_secs(1);
let send_ts = ClientTimestamp {
@@ -1550,7 +1512,7 @@ fn matched_reply_with_reversed_monotonic_time_still_emits_event() {
other => panic!("expected EchoReply, got {other:?}"),
}
client.close(ClientTimestamp::now()).unwrap();
client.close().unwrap();
server.join();
}
@@ -1566,7 +1528,7 @@ fn process_received_packet_uses_supplied_receive_metadata() {
..default_test_config(server.addr)
};
let mut client = Client::connect(config).unwrap();
assert_open_started(client.open(ClientTimestamp::now()).unwrap());
assert_open_started(client.open().unwrap());
client.send_probe_at(ClientTimestamp::now()).unwrap();
let recv_ts = ClientTimestamp::now();
@@ -1596,7 +1558,7 @@ fn process_received_packet_uses_supplied_receive_metadata() {
other => panic!("expected EchoReply, got {other:?}"),
}
client.close(ClientTimestamp::now()).unwrap();
client.close().unwrap();
server.join();
}
@@ -1613,11 +1575,11 @@ fn process_received_packet_uses_supplied_receive_metadata_for_late_reply() {
..default_test_config(server.addr)
};
let mut client = Client::connect(config).unwrap();
assert_open_started(client.open(ClientTimestamp::now()).unwrap());
assert_open_started(client.open().unwrap());
client.send_probe_at(ClientTimestamp::now()).unwrap();
thread::sleep(Duration::from_millis(60));
let losses = client.poll_timeouts(ClientTimestamp::now()).unwrap();
let losses = client.poll_timeouts().unwrap();
assert!(matches!(&losses[0], ClientEvent::EchoLoss { seq: 0, .. }));
let recv_ts = ClientTimestamp::now();
@@ -1647,7 +1609,7 @@ fn process_received_packet_uses_supplied_receive_metadata_for_late_reply() {
other => panic!("expected LateReply, got {other:?}"),
}
client.close(ClientTimestamp::now()).unwrap();
client.close().unwrap();
server.join();
}
@@ -1663,7 +1625,7 @@ fn receive_metadata_does_not_broaden_malformed_warning() {
..default_test_config(server.addr)
};
let mut client = Client::connect(config).unwrap();
assert_open_started(client.open(ClientTimestamp::now()).unwrap());
assert_open_started(client.open().unwrap());
let events = client
.process_received_packet(
@@ -1678,14 +1640,14 @@ fn receive_metadata_does_not_broaden_malformed_warning() {
assert_eq!(events.len(), 1);
match &events[0] {
ClientEvent::Warning { kind, message } => {
ClientEvent::Warning { kind, message, .. } => {
assert_eq!(*kind, WarningKind::MalformedOrUnrelatedPacket);
assert_eq!(message, "dropped malformed or unrelated packet");
}
other => panic!("expected Warning, got {other:?}"),
}
client.close(ClientTimestamp::now()).unwrap();
client.close().unwrap();
server.join();
}
@@ -1713,7 +1675,7 @@ fn recv_available_drains_burst_replies() {
));
}
client.close(ClientTimestamp::now()).unwrap();
client.close().unwrap();
server.join();
}
@@ -1738,7 +1700,7 @@ fn recv_available_respects_packet_budget() {
assert_eq!(second.len(), 1);
assert!(matches!(&second[0], ClientEvent::EchoReply { seq: 1, .. }));
client.close(ClientTimestamp::now()).unwrap();
client.close().unwrap();
server.join();
}
@@ -1754,11 +1716,10 @@ fn send_probe_wraps_wire_sequence_at_u32_max() {
..default_test_config(server.addr)
};
let mut client = Client::connect(config).unwrap();
assert_open_started(client.open(ClientTimestamp::now()).unwrap());
assert_open_started(client.open().unwrap());
let session = client.session.as_mut().unwrap();
session.next_wire_seq = u32::MAX;
session.next_logical_seq = 41;
client.send_probe().unwrap();
client.send_probe().unwrap();
@@ -1772,7 +1733,7 @@ fn send_probe_wraps_wire_sequence_at_u32_max() {
.collect();
assert_eq!(seqs, vec![u32::MAX, 0]);
client.close(ClientTimestamp::now()).unwrap();
client.close().unwrap();
server.join();
}
@@ -1811,10 +1772,9 @@ fn wrapped_reply_after_u32_max_is_not_late() {
..default_test_config(server.addr)
};
let mut client = Client::connect(config).unwrap();
assert_open_started(client.open(ClientTimestamp::now()).unwrap());
assert_open_started(client.open().unwrap());
let session = client.session.as_mut().unwrap();
session.next_wire_seq = u32::MAX;
session.next_logical_seq = 41;
client.send_probe().unwrap();
client.send_probe().unwrap();
@@ -1848,7 +1808,7 @@ fn send_probe_after_sending_done_is_noop() {
..default_test_config(server.addr)
};
let mut client = Client::connect(config).unwrap();
assert_open_started(client.open(ClientTimestamp::now()).unwrap());
assert_open_started(client.open().unwrap());
client.session.as_mut().unwrap().sending_done = true;
let events = client.send_probe().unwrap();
@@ -1863,7 +1823,7 @@ fn send_probe_after_sending_done_is_noop() {
.count();
assert_eq!(echo_count, 0);
client.close(ClientTimestamp::now()).unwrap();
client.close().unwrap();
server.join();
}
@@ -1926,7 +1886,7 @@ fn recv_buffer_uses_negotiated_packet_length() {
..default_test_config(server.addr)
};
let mut client = Client::connect(config).unwrap();
assert_open_started(client.open(ClientTimestamp::now()).unwrap());
assert_open_started(client.open().unwrap());
let buf_size = client.recv_buffer_size();
assert!(
buf_size > 4096,
@@ -1934,7 +1894,7 @@ fn recv_buffer_uses_negotiated_packet_length() {
);
assert_eq!(buf_size, 4097);
assert_eq!(client.recv_buffer.len(), buf_size);
client.close(ClientTimestamp::now()).unwrap();
client.close().unwrap();
server.join();
}
@@ -1954,7 +1914,7 @@ fn short_echo_reply_does_not_emit_echo_reply() {
..default_test_config(server.addr)
};
let mut client = Client::connect(config).unwrap();
assert_open_started(client.open(ClientTimestamp::now()).unwrap());
assert_open_started(client.open().unwrap());
client.send_probe().unwrap();
let mut packet = echo_reply_packet(TOKEN, 0, &params, &TimestampFields::default(), None);
@@ -1970,7 +1930,7 @@ fn short_echo_reply_does_not_emit_echo_reply() {
..
}]
));
client.close(ClientTimestamp::now()).unwrap();
client.close().unwrap();
server.join();
}
@@ -1986,7 +1946,7 @@ fn overlong_echo_reply_does_not_emit_echo_reply() {
..default_test_config(server.addr)
};
let mut client = Client::connect(config).unwrap();
assert_open_started(client.open(ClientTimestamp::now()).unwrap());
assert_open_started(client.open().unwrap());
client.send_probe().unwrap();
let mut packet = echo_reply_packet(TOKEN, 0, &params, &TimestampFields::default(), None);
@@ -2002,7 +1962,7 @@ fn overlong_echo_reply_does_not_emit_echo_reply() {
..
}]
));
client.close(ClientTimestamp::now()).unwrap();
client.close().unwrap();
server.join();
}
@@ -2036,7 +1996,7 @@ fn overlong_datagram_detection_uses_extra_receive_byte() {
..default_test_config(server.addr)
};
let mut client = Client::connect(config).unwrap();
assert_open_started(client.open(ClientTimestamp::now()).unwrap());
assert_open_started(client.open().unwrap());
assert_eq!(
client.recv_buffer.len(),
echo_packet_len(false, &params) + 1
@@ -2051,7 +2011,7 @@ fn overlong_datagram_detection_uses_extra_receive_byte() {
..
}]
));
client.close(ClientTimestamp::now()).unwrap();
client.close().unwrap();
server.join();
}
@@ -2083,7 +2043,7 @@ fn exact_length_echo_reply_still_emits_echo_reply() {
..default_test_config(server.addr)
};
let mut client = Client::connect(config).unwrap();
assert_open_started(client.open(ClientTimestamp::now()).unwrap());
assert_open_started(client.open().unwrap());
client.send_probe().unwrap();
let events = client.recv_once().unwrap();
@@ -2091,7 +2051,7 @@ fn exact_length_echo_reply_still_emits_echo_reply() {
events.as_slice(),
[ClientEvent::EchoReply { bytes, .. }] if *bytes == echo_packet_len(false, &params)
));
client.close(ClientTimestamp::now()).unwrap();
client.close().unwrap();
server.join();
}
@@ -2113,6 +2073,6 @@ fn recv_once_at_test_helper_provides_deterministic_timestamp() {
} else {
panic!("expected EchoReply");
}
client.close(ClientTimestamp::now()).unwrap();
client.close().unwrap();
server.join();
}
@@ -26,7 +26,6 @@ fn sequence_ordering_handles_normal_and_wrapped_values() {
fn highest_received_seq_updates_across_wrap() {
let mut session = ActiveSession {
next_wire_seq: 0,
next_logical_seq: 0,
highest_received_seq: Some(u32::MAX),
packets_sent: 0,
start_mono: Instant::now(),
@@ -1,7 +1,8 @@
use super::*;
use irtt_proto::{
compute_hmac_in_place, echo_packet_len, flags::FLAG_HMAC, flags::FLAG_OPEN, flags::FLAG_REPLY,
layout::PacketLayout, Clock, ReceivedStats, StampAt, HMAC_SIZE, MAGIC,
compute_hmac_in_place, echo_packet_len as checked_echo_packet_len, flags::FLAG_HMAC,
flags::FLAG_OPEN, flags::FLAG_REPLY, layout::PacketLayout, Clock, ReceivedStats, StampAt,
HMAC_SIZE, MAGIC,
};
use std::{
net::UdpSocket,
@@ -74,6 +75,11 @@ pub(super) fn open_reply(
packet
}
pub(crate) fn echo_packet_len(hmac: bool, params: &Params) -> usize {
checked_echo_packet_len(hmac, params)
.expect("test params must have a non-negative packet length")
}
pub(super) fn echo_reply_packet(
token: u64,
seq: u32,
@@ -283,7 +289,7 @@ pub(super) fn open_client_with_echo_server(params: &Params) -> (Client, FakeServ
..default_test_config(server.addr)
};
let mut client = Client::connect(config).unwrap();
assert_open_started(client.open(ClientTimestamp::now()).unwrap());
assert_open_started(client.open().unwrap());
(client, server)
}
+2 -2
View File
@@ -29,10 +29,10 @@ fn connect_applies_configured_ipv4_ttl_before_open_and_close_preserves_it() {
assert_eq!(socket_ttl(&client.socket, client.remote).unwrap(), 64);
client.open(ClientTimestamp::now()).unwrap();
client.open().unwrap();
server.join();
client.close(ClientTimestamp::now()).unwrap();
client.close().unwrap();
assert_eq!(socket_ttl(&client.socket, client.remote).unwrap(), 64);
}
+1
View File
@@ -3,6 +3,7 @@ use std::{io, time::Duration};
use thiserror::Error;
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum ClientError {
#[error("failed to resolve server address {addr:?}")]
Resolve { addr: String },
+2 -4
View File
@@ -41,7 +41,6 @@ pub enum ClientEvent {
EchoSent {
seq: u32,
logical_seq: u64,
remote: SocketAddr,
scheduled_at: Instant,
sent_at: ClientTimestamp,
@@ -52,7 +51,6 @@ pub enum ClientEvent {
EchoReply {
seq: u32,
logical_seq: u64,
remote: SocketAddr,
sent_at: ClientTimestamp,
received_at: ClientTimestamp,
@@ -66,7 +64,6 @@ pub enum ClientEvent {
EchoLoss {
seq: u32,
logical_seq: u64,
sent_at: ClientTimestamp,
timeout_at: std::time::Instant,
},
@@ -80,7 +77,6 @@ pub enum ClientEvent {
LateReply {
seq: u32,
logical_seq: Option<u64>,
highest_seen: u32,
remote: SocketAddr,
sent_at: Option<ClientTimestamp>,
@@ -96,10 +92,12 @@ pub enum ClientEvent {
Warning {
kind: WarningKind,
message: String,
at: ClientTimestamp,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum WarningKind {
MalformedOrUnrelatedPacket,
WrongToken,
+1 -1
View File
@@ -31,5 +31,5 @@ pub use managed::{
CancellationToken, EventHub, EventSubscription, ManagedClient, ManagedClientSession,
SessionEndReason, SessionOutcome, SubscriberConfig, SubscriberOverflow,
};
pub use session::NegotiatedParams;
pub use session::{NegotiatedParams, NegotiationRestriction};
pub use timing::ClientTimestamp;
+1
View File
@@ -279,6 +279,7 @@ mod tests {
ClientEvent::Warning {
kind: WarningKind::UntrackedReply,
message: format!("event-{n}"),
at: ClientTimestamp::now(),
}
}
+18 -26
View File
@@ -7,7 +7,6 @@ use crate::{
config::{ClientConfig, RecvBudget},
error::ClientError,
event::{ClientEvent, OpenOutcome},
timing::ClientTimestamp,
Client,
};
@@ -94,7 +93,7 @@ impl ManagedClient {
.transpose()?;
let mut client = Client::connect(config)?;
let outcome = client.open(ClientTimestamp::now())?;
let outcome = client.open()?;
publish_open_outcome(&hub, &outcome);
let cancellation = CancellationToken::new();
@@ -182,11 +181,7 @@ fn run_client(
&mut counters,
client.recv_available(MANAGED_RECV_BUDGET)?,
);
publish_events(
&hub,
&mut counters,
client.poll_timeouts(ClientTimestamp::now())?,
);
publish_events(&hub, &mut counters, client.poll_timeouts()?);
break;
}
@@ -205,11 +200,7 @@ fn run_client(
&mut counters,
client.recv_available(MANAGED_RECV_BUDGET)?,
);
publish_events(
&hub,
&mut counters,
client.poll_timeouts(ClientTimestamp::now())?,
);
publish_events(&hub, &mut counters, client.poll_timeouts()?);
if client.is_run_complete() {
break;
@@ -223,7 +214,7 @@ fn run_client(
}
let packets_sent = client.packets_sent();
let close_events = client.close(ClientTimestamp::now())?;
let close_events = client.close()?;
publish_events(&hub, &mut counters, close_events);
Ok(SessionOutcome {
@@ -267,7 +258,7 @@ fn drain_final_late_replies(
published |= !events.is_empty();
publish_events(hub, counters, events);
let events = client.poll_timeouts(ClientTimestamp::now())?;
let events = client.poll_timeouts()?;
published |= !events.is_empty();
publish_events(hub, counters, events);
@@ -320,8 +311,10 @@ mod tests {
use super::*;
use crate::{config::NegotiationPolicy, SubscriberOverflow};
use irtt_proto::{
echo_packet_len, flags, flags::FLAG_OPEN, flags::FLAG_REPLY, layout::PacketLayout, Clock,
Params, ReceivedStats, StampAt, TimestampFields, MAGIC, PROTOCOL_VERSION,
echo_packet_len,
flags::{self, FLAG_OPEN, FLAG_REPLY},
layout::PacketLayout,
Clock, Params, ReceivedStats, StampAt, TimestampFields, MAGIC, PROTOCOL_VERSION,
};
use std::{
net::{SocketAddr, UdpSocket},
@@ -340,6 +333,11 @@ mod tests {
}
}
fn test_echo_packet_len(hmac: bool, params: &Params) -> usize {
echo_packet_len(hmac, params)
.expect("managed runner test params must have a non-negative packet length")
}
fn test_params(duration: Option<Duration>, interval: Duration) -> Params {
Params {
protocol_version: PROTOCOL_VERSION,
@@ -470,7 +468,7 @@ mod tests {
timestamps: &TimestampFields,
) -> Vec<u8> {
let layout = PacketLayout::echo(false, params);
let packet_len = echo_packet_len(false, params);
let packet_len = test_echo_packet_len(false, params);
let mut packet = Vec::with_capacity(packet_len);
packet.extend_from_slice(&MAGIC);
@@ -603,19 +601,13 @@ mod tests {
server.join();
assert_eq!(outcome.end_reason, SessionEndReason::TestComplete);
assert!(events.iter().any(|event| matches!(
event,
ClientEvent::EchoLoss {
seq: 0,
logical_seq: 0,
..
}
)));
assert!(events
.iter()
.any(|event| matches!(event, ClientEvent::EchoLoss { seq: 0, .. })));
assert!(events.iter().any(|event| matches!(
event,
ClientEvent::LateReply {
seq: 0,
logical_seq: Some(0),
sent_at: Some(_),
rtt: Some(_),
..
+27 -46
View File
@@ -1,5 +1,5 @@
use std::{
collections::{HashMap, VecDeque},
collections::{HashMap, HashSet, VecDeque},
time::Instant,
};
@@ -7,7 +7,6 @@ use crate::{error::ClientError, timing::ClientTimestamp};
#[derive(Debug, Clone)]
pub(crate) struct PendingProbe {
pub logical_seq: u64,
pub wire_seq: u32,
pub sent_at: ClientTimestamp,
pub timeout_at: Instant,
@@ -59,11 +58,10 @@ impl PendingMap {
expired.push(probe);
}
}
expired.sort_by_key(|p| p.logical_seq);
expired.sort_by_key(|p| p.sent_at.mono);
expired
}
#[allow(dead_code)]
pub fn len(&self) -> usize {
self.map.len()
}
@@ -115,7 +113,6 @@ impl TimedOutMap {
self.insertion_order.clear();
}
#[allow(dead_code)]
pub fn len(&self) -> usize {
self.map.len()
}
@@ -136,7 +133,7 @@ impl TimedOutMap {
#[derive(Debug)]
pub(crate) struct CompletedSet {
set: HashMap<u32, u64>,
set: HashSet<u32>,
insertion_order: VecDeque<u32>,
max_capacity: usize,
}
@@ -144,31 +141,32 @@ pub(crate) struct CompletedSet {
impl CompletedSet {
pub fn new(max_capacity: usize) -> Self {
Self {
set: HashMap::new(),
set: HashSet::new(),
insertion_order: VecDeque::new(),
max_capacity,
}
}
pub fn insert(&mut self, wire_seq: u32, logical_seq: u64) {
if let Some(existing) = self.set.get_mut(&wire_seq) {
*existing = logical_seq;
pub fn insert(&mut self, seq: u32) {
if self.set.contains(&seq) {
return;
}
if self.set.len() >= self.max_capacity {
self.evict_oldest();
}
self.insertion_order.push_back(wire_seq);
self.set.insert(wire_seq, logical_seq);
self.insertion_order.push_back(seq);
self.set.insert(seq);
}
pub fn contains(&self, wire_seq: u32) -> bool {
self.set.contains_key(&wire_seq)
pub fn contains(&self, seq: u32) -> bool {
self.set.contains(&seq)
}
fn evict_oldest(&mut self) {
while let Some(oldest_key) = self.insertion_order.pop_front() {
if self.set.remove(&oldest_key).is_some() {
while let Some(oldest_seq) = self.insertion_order.pop_front() {
if self.set.remove(&oldest_seq) {
break;
}
}
@@ -187,9 +185,8 @@ mod tests {
}
}
fn pending(seq: u32, logical: u64, timeout_at: Instant) -> PendingProbe {
fn pending(seq: u32, timeout_at: Instant) -> PendingProbe {
PendingProbe {
logical_seq: logical,
wire_seq: seq,
sent_at: ts(timeout_at - Duration::from_secs(1)),
timeout_at,
@@ -200,7 +197,7 @@ mod tests {
fn pending_map_insert_and_remove() {
let mut map = PendingMap::new(10);
let now = Instant::now();
let probe = pending(0, 0, now + Duration::from_secs(4));
let probe = pending(0, now + Duration::from_secs(4));
map.insert(probe).unwrap();
assert_eq!(map.len(), 1);
assert!(map.remove(0).is_some());
@@ -212,42 +209,26 @@ mod tests {
fn pending_map_rejects_over_capacity() {
let mut map = PendingMap::new(2);
let now = Instant::now();
map.insert(pending(0, 0, now + Duration::from_secs(4)))
map.insert(pending(0, now + Duration::from_secs(4)))
.unwrap();
map.insert(pending(1, 1, now + Duration::from_secs(4)))
map.insert(pending(1, now + Duration::from_secs(4)))
.unwrap();
assert!(matches!(
map.insert(pending(2, 2, now + Duration::from_secs(4))),
map.insert(pending(2, now + Duration::from_secs(4))),
Err(ClientError::PendingLimitExceeded { limit: 2 })
));
}
#[test]
fn drain_expired_returns_in_logical_order() {
let mut map = PendingMap::new(10);
let now = Instant::now();
let past = now - Duration::from_secs(1);
map.insert(pending(5, 5, past)).unwrap();
map.insert(pending(2, 2, past)).unwrap();
map.insert(pending(8, 8, now + Duration::from_secs(10)))
.unwrap();
let expired = map.drain_expired(now);
assert_eq!(expired.len(), 2);
assert_eq!(expired[0].wire_seq, 2);
assert_eq!(expired[1].wire_seq, 5);
assert_eq!(map.len(), 1);
}
#[test]
fn completed_set_tracks_and_evicts() {
let mut set = CompletedSet::new(3);
set.insert(0, 0);
set.insert(1, 1);
set.insert(2, 2);
set.insert(0);
set.insert(1);
set.insert(2);
assert!(set.contains(0));
assert!(set.contains(1));
assert!(set.contains(2));
set.insert(3, 3);
set.insert(3);
assert_eq!(set.set.len(), 3);
assert!(!set.contains(0));
assert!(set.contains(3));
@@ -257,9 +238,9 @@ mod tests {
fn timed_out_map_tracks_and_evicts() {
let mut map = TimedOutMap::new(2);
let now = Instant::now();
map.insert(pending(0, 0, now));
map.insert(pending(1, 1, now));
map.insert(pending(2, 2, now));
map.insert(pending(0, now));
map.insert(pending(1, now));
map.insert(pending(2, now));
assert_eq!(map.len(), 2);
assert!(map.remove(0).is_none());
@@ -273,7 +254,7 @@ mod tests {
let now = Instant::now();
for i in 0..20 {
map.insert(pending(i, u64::from(i), now));
map.insert(pending(i, now));
assert!(map.remove(i).is_some());
assert_eq!(map.len(), 0);
assert_eq!(map.insertion_order_len(), 0);
+1 -1
View File
@@ -15,7 +15,7 @@ pub(crate) fn configure_receive_metadata(socket: &UdpSocket, remote: SocketAddr)
enable_kernel_rx_timestamps(socket)?;
let socket = socket2::SockRef::from(socket);
if remote.is_ipv4() {
socket.set_recv_tos(true)
socket.set_recv_tos_v4(true)
} else {
socket.set_recv_tclass_v6(true)
}
+328 -26
View File
@@ -1,6 +1,6 @@
use std::time::Instant;
use std::{fmt, time::Instant};
use irtt_proto::{Params, PROTOCOL_VERSION};
use irtt_proto::{Clock, Params, ReceivedStats, StampAt, PROTOCOL_VERSION};
use crate::{
config::{NegotiationPolicy, MAX_DSCP_CODEPOINT},
@@ -11,6 +11,141 @@ use crate::{
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct NegotiatedParams {
pub params: Params,
pub restrictions: Vec<NegotiationRestriction>,
}
/// A server-side restriction applied during session parameter negotiation.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum NegotiationRestriction {
/// Run duration was reduced.
///
/// A requested duration of `0` means the client requested continuous mode.
/// When `requested_ns == 0` and `negotiated_ns > 0`, the server limited
/// that continuous request to a finite duration.
DurationReduced {
requested_ns: i64,
negotiated_ns: i64,
},
/// Probe interval was increased.
IntervalIncreased {
requested_ns: i64,
negotiated_ns: i64,
},
/// Probe interval was reduced.
IntervalReduced {
requested_ns: i64,
negotiated_ns: i64,
},
/// Packet length was reduced.
LengthReduced { requested: i64, negotiated: i64 },
/// Returned received-statistics mode differs from the request.
ReceivedStatsChanged {
requested: ReceivedStats,
negotiated: ReceivedStats,
},
/// Returned timestamp placement differs from the request.
StampAtChanged {
requested: StampAt,
negotiated: StampAt,
},
/// Returned clock source differs from the request.
ClockChanged { requested: Clock, negotiated: Clock },
/// Returned DSCP codepoint differs from the request.
DscpChanged { requested: i64, negotiated: i64 },
/// Returned server payload fill behavior differs from the request.
ServerFillChanged,
}
impl NegotiationRestriction {
pub fn message(&self) -> String {
self.to_string()
}
}
impl fmt::Display for NegotiationRestriction {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::DurationReduced {
requested_ns: 0,
negotiated_ns,
} => {
write!(
f,
"server limited continuous duration to {negotiated_ns} ns"
)
}
Self::DurationReduced {
requested_ns,
negotiated_ns,
} => {
write!(
f,
"server reduced duration from {requested_ns} ns to {negotiated_ns} ns"
)
}
Self::IntervalIncreased {
requested_ns,
negotiated_ns,
} => {
write!(
f,
"server increased interval from {requested_ns} ns to {negotiated_ns} ns"
)
}
Self::IntervalReduced {
requested_ns,
negotiated_ns,
} => {
write!(
f,
"server reduced interval from {requested_ns} ns to {negotiated_ns} ns"
)
}
Self::LengthReduced {
requested,
negotiated,
} => {
write!(
f,
"server reduced packet length from {requested} bytes to {negotiated} bytes"
)
}
Self::ReceivedStatsChanged {
requested,
negotiated,
} => {
write!(
f,
"server changed received-stats from {requested:?} to {negotiated:?}"
)
}
Self::StampAtChanged {
requested,
negotiated,
} => {
write!(
f,
"server changed stamp-at from {requested:?} to {negotiated:?}"
)
}
Self::ClockChanged {
requested,
negotiated,
} => {
write!(
f,
"server changed clock from {requested:?} to {negotiated:?}"
)
}
Self::DscpChanged {
requested,
negotiated,
} => {
write!(f, "server changed DSCP from {requested} to {negotiated}")
}
Self::ServerFillChanged => write!(f, "server changed payload fill behavior"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
@@ -24,7 +159,6 @@ pub(crate) enum ClientPhase {
#[derive(Debug)]
pub(crate) struct ActiveSession {
pub next_wire_seq: u32,
pub next_logical_seq: u64,
pub highest_received_seq: Option<u32>,
pub packets_sent: u64,
pub start_mono: Instant,
@@ -36,17 +170,19 @@ pub(crate) struct ActiveSession {
pub sending_done: bool,
}
pub(crate) fn validate_negotiated_params(
pub(crate) fn negotiate_params(
requested: &Params,
returned: &Params,
returned: Params,
policy: NegotiationPolicy,
) -> Result<(), ClientError> {
) -> Result<NegotiatedParams, ClientError> {
if returned.protocol_version != PROTOCOL_VERSION {
return Err(ClientError::ProtocolVersionMismatch {
requested: PROTOCOL_VERSION,
received: returned.protocol_version,
});
}
let mut restrictions = Vec::new();
validate_duration_restriction(requested.duration_ns, returned.duration_ns)?;
if returned.length < 0 {
return Err(ClientError::NegotiationRejected {
@@ -65,12 +201,105 @@ pub(crate) fn validate_negotiated_params(
}
validate_dscp_restriction(returned.dscp)?;
if policy == NegotiationPolicy::Strict && returned != requested {
if returned.duration_ns < requested.duration_ns
|| (requested.duration_ns == 0 && returned.duration_ns > 0)
{
record_restriction(
policy,
&mut restrictions,
NegotiationRestriction::DurationReduced {
requested_ns: requested.duration_ns,
negotiated_ns: returned.duration_ns,
},
)?;
}
if returned.interval_ns > requested.interval_ns {
record_restriction(
policy,
&mut restrictions,
NegotiationRestriction::IntervalIncreased {
requested_ns: requested.interval_ns,
negotiated_ns: returned.interval_ns,
},
)?;
}
if returned.interval_ns < requested.interval_ns {
record_restriction(
policy,
&mut restrictions,
NegotiationRestriction::IntervalReduced {
requested_ns: requested.interval_ns,
negotiated_ns: returned.interval_ns,
},
)?;
}
if returned.length < requested.length {
record_restriction(
policy,
&mut restrictions,
NegotiationRestriction::LengthReduced {
requested: requested.length,
negotiated: returned.length,
},
)?;
}
if returned.received_stats != requested.received_stats {
record_restriction(
policy,
&mut restrictions,
NegotiationRestriction::ReceivedStatsChanged {
requested: requested.received_stats,
negotiated: returned.received_stats,
},
)?;
}
if returned.stamp_at != requested.stamp_at {
record_restriction(
policy,
&mut restrictions,
NegotiationRestriction::StampAtChanged {
requested: requested.stamp_at,
negotiated: returned.stamp_at,
},
)?;
}
if returned.clock != requested.clock {
record_restriction(
policy,
&mut restrictions,
NegotiationRestriction::ClockChanged {
requested: requested.clock,
negotiated: returned.clock,
},
)?;
}
if returned.dscp != requested.dscp && returned.dscp != 0 {
return Err(ClientError::NegotiationRejected {
reason: "returned params differ from requested params".to_owned(),
reason: "server returned unsupported DSCP change".to_owned(),
});
}
Ok(())
if returned.dscp == 0 && requested.dscp != 0 {
record_restriction(
policy,
&mut restrictions,
NegotiationRestriction::DscpChanged {
requested: requested.dscp,
negotiated: returned.dscp,
},
)?;
}
if returned.server_fill != requested.server_fill {
record_restriction(
policy,
&mut restrictions,
NegotiationRestriction::ServerFillChanged,
)?;
}
Ok(NegotiatedParams {
params: returned,
restrictions,
})
}
fn validate_duration_restriction(requested: i64, returned: i64) -> Result<(), ClientError> {
@@ -105,6 +334,21 @@ fn validate_dscp_restriction(returned: i64) -> Result<(), ClientError> {
Ok(())
}
fn record_restriction(
policy: NegotiationPolicy,
restrictions: &mut Vec<NegotiationRestriction>,
restriction: NegotiationRestriction,
) -> Result<(), ClientError> {
if policy == NegotiationPolicy::Strict {
return Err(ClientError::NegotiationRejected {
reason: restriction.message(),
});
}
restrictions.push(restriction);
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
@@ -128,17 +372,26 @@ mod tests {
fn assert_rejected(requested: &Params, returned: &Params, policy: NegotiationPolicy) {
assert!(matches!(
validate_negotiated_params(requested, returned, policy),
negotiate_params(requested, returned.clone(), policy),
Err(ClientError::NegotiationRejected { .. })
));
}
fn assert_negotiates(
requested: &Params,
returned: &Params,
policy: NegotiationPolicy,
) -> NegotiatedParams {
negotiate_params(requested, returned.clone(), policy)
.unwrap_or_else(|err| panic!("expected negotiation success, got {err:?}"))
}
fn rejection_reason(
requested: &Params,
returned: &Params,
policy: NegotiationPolicy,
) -> String {
match validate_negotiated_params(requested, returned, policy) {
match negotiate_params(requested, returned.clone(), policy) {
Err(ClientError::NegotiationRejected { reason }) => reason,
other => panic!("expected negotiation rejection, got {other:?}"),
}
@@ -150,27 +403,65 @@ mod tests {
let mut returned = requested.clone();
returned.length = 128;
assert_rejected(&requested, &returned, NegotiationPolicy::Strict);
assert_eq!(
rejection_reason(&requested, &returned, NegotiationPolicy::Strict),
NegotiationRestriction::LengthReduced {
requested: requested.length,
negotiated: returned.length,
}
.message()
);
let mut returned = requested.clone();
returned.dscp = 8;
assert_rejected(&requested, &returned, NegotiationPolicy::Strict);
returned.dscp = 0;
assert_eq!(
rejection_reason(&requested, &returned, NegotiationPolicy::Strict),
NegotiationRestriction::DscpChanged {
requested: requested.dscp,
negotiated: returned.dscp,
}
.message()
);
let mut returned = requested.clone();
returned.received_stats = ReceivedStats::Count;
assert_rejected(&requested, &returned, NegotiationPolicy::Strict);
assert_eq!(
rejection_reason(&requested, &returned, NegotiationPolicy::Strict),
NegotiationRestriction::ReceivedStatsChanged {
requested: requested.received_stats,
negotiated: returned.received_stats,
}
.message()
);
let mut returned = requested.clone();
returned.stamp_at = StampAt::Midpoint;
assert_rejected(&requested, &returned, NegotiationPolicy::Strict);
assert_eq!(
rejection_reason(&requested, &returned, NegotiationPolicy::Strict),
NegotiationRestriction::StampAtChanged {
requested: requested.stamp_at,
negotiated: returned.stamp_at,
}
.message()
);
let mut returned = requested.clone();
returned.clock = Clock::Wall;
assert_rejected(&requested, &returned, NegotiationPolicy::Strict);
assert_eq!(
rejection_reason(&requested, &returned, NegotiationPolicy::Strict),
NegotiationRestriction::ClockChanged {
requested: requested.clock,
negotiated: returned.clock,
}
.message()
);
let mut returned = requested.clone();
returned.server_fill = None;
assert_rejected(&requested, &returned, NegotiationPolicy::Strict);
assert_eq!(
rejection_reason(&requested, &returned, NegotiationPolicy::Strict),
NegotiationRestriction::ServerFillChanged.message()
);
}
#[test]
@@ -179,8 +470,13 @@ mod tests {
let mut returned = requested.clone();
returned.duration_ns = requested.duration_ns / 2;
assert!(
validate_negotiated_params(&requested, &returned, NegotiationPolicy::Loose).is_ok()
let negotiated = assert_negotiates(&requested, &returned, NegotiationPolicy::Loose);
assert_eq!(
negotiated.restrictions,
vec![NegotiationRestriction::DurationReduced {
requested_ns: requested.duration_ns,
negotiated_ns: returned.duration_ns,
}]
);
let mut returned = requested.clone();
@@ -201,12 +497,18 @@ mod tests {
continuous_requested.duration_ns = 0;
let mut finite_returned = continuous_requested.clone();
finite_returned.duration_ns = 1_000_000_000;
assert!(validate_negotiated_params(
let negotiated = assert_negotiates(
&continuous_requested,
&finite_returned,
NegotiationPolicy::Loose
)
.is_ok());
NegotiationPolicy::Loose,
);
assert_eq!(
negotiated.restrictions,
vec![NegotiationRestriction::DurationReduced {
requested_ns: 0,
negotiated_ns: finite_returned.duration_ns,
}]
);
assert_rejected(
&continuous_requested,
@@ -214,9 +516,9 @@ mod tests {
NegotiationPolicy::Strict,
);
assert!(validate_negotiated_params(
&continuous_requested,
assert!(negotiate_params(
&continuous_requested,
continuous_requested.clone(),
NegotiationPolicy::Strict
)
.is_ok());
+4 -4
View File
@@ -78,7 +78,7 @@ pub(crate) fn socket_ttl(socket: &UdpSocket, remote: SocketAddr) -> Result<u32,
fn set_socket_ttl(socket: &UdpSocket, remote: SocketAddr, ttl: u32) -> io::Result<()> {
if remote.is_ipv4() {
socket2::SockRef::from(socket).set_ttl(ttl)
socket2::SockRef::from(socket).set_ttl_v4(ttl)
} else {
socket2::SockRef::from(socket).set_unicast_hops_v6(ttl)
}
@@ -87,7 +87,7 @@ fn set_socket_ttl(socket: &UdpSocket, remote: SocketAddr, ttl: u32) -> io::Resul
#[cfg(test)]
fn get_socket_ttl(socket: &UdpSocket, remote: SocketAddr) -> io::Result<u32> {
if remote.is_ipv4() {
socket2::SockRef::from(socket).ttl()
socket2::SockRef::from(socket).ttl_v4()
} else {
socket2::SockRef::from(socket).unicast_hops_v6()
}
@@ -128,7 +128,7 @@ fn set_socket_traffic_class_io(
target_os = "haiku",
)))]
fn set_ipv4_traffic_class(socket: &UdpSocket, traffic_class: u32) -> io::Result<()> {
socket2::SockRef::from(socket).set_tos(traffic_class)
socket2::SockRef::from(socket).set_tos_v4(traffic_class)
}
#[cfg(any(
@@ -212,7 +212,7 @@ fn unsupported_traffic_class(traffic_class: u32, feature: &'static str) -> io::R
))
))]
fn get_ipv4_traffic_class(socket: &UdpSocket) -> io::Result<u32> {
socket2::SockRef::from(socket).tos()
socket2::SockRef::from(socket).tos_v4()
}
#[cfg(all(
+17 -30
View File
@@ -3,10 +3,9 @@ mod support;
use std::time::Duration;
use irtt_client::{
Client, ClientConfig, ClientError, ClientEvent, ClientTimestamp, OpenOutcome, SocketConfig,
WarningKind,
Client, ClientConfig, ClientError, ClientEvent, OpenOutcome, SocketConfig, WarningKind,
};
use irtt_proto::{echo_packet_len, ProtoError, TimestampFields};
use irtt_proto::{ProtoError, TimestampFields};
use support::{
config_for_params, default_params, params_for_modes, run_one_probe_with_config,
@@ -14,6 +13,8 @@ use support::{
start_hmac_required_open_drop_server, BackendPeer, ServerObservation, TOKEN,
};
use crate::support::test_echo_packet_len;
#[test]
fn hmac_open_success_negotiates_without_warnings() {
let key = b"compat-secret".to_vec();
@@ -63,7 +64,7 @@ fn hmac_echo_success_verifies_request_and_accepts_reply() {
token,
sequence,
} => {
assert_eq!(*len, echo_packet_len(true, &params));
assert_eq!(*len, test_echo_packet_len(true, &params));
assert!(*hmac);
assert_eq!(*token, TOKEN);
assert_eq!(*sequence, 0);
@@ -74,14 +75,12 @@ fn hmac_echo_success_verifies_request_and_accepts_reply() {
match &run.reply {
ClientEvent::EchoReply {
seq,
logical_seq,
bytes,
server_timing,
..
} => {
assert_eq!(*seq, 0);
assert_eq!(*logical_seq, 0);
assert_eq!(*bytes, echo_packet_len(true, &params));
assert_eq!(*bytes, test_echo_packet_len(true, &params));
assert!(server_timing.is_some());
}
other => panic!("expected EchoReply, got {other:?}"),
@@ -98,10 +97,10 @@ fn hmac_close_success_sends_authenticated_close_and_closes_session() {
config.hmac_key = Some(key);
let mut client = Client::connect(config).unwrap();
let outcome = client.open(ClientTimestamp::now()).unwrap();
let outcome = client.open().unwrap();
assert_started(outcome, &params);
let events = client.close(ClientTimestamp::now()).unwrap();
let events = client.close().unwrap();
assert!(matches!(
events.as_slice(),
[ClientEvent::SessionClosed { token: TOKEN, .. }]
@@ -131,10 +130,7 @@ fn missing_client_key_against_hmac_required_server_times_out() {
config.hmac_key = None;
let mut client = Client::connect(config).unwrap();
assert!(matches!(
client.open(ClientTimestamp::now()),
Err(ClientError::OpenTimeout)
));
assert!(matches!(client.open(), Err(ClientError::OpenTimeout)));
let observations = server.observations(1);
assert!(matches!(
@@ -156,10 +152,7 @@ fn wrong_client_key_against_hmac_required_server_times_out() {
config.hmac_key = Some(b"wrong-secret".to_vec());
let mut client = Client::connect(config).unwrap();
assert!(matches!(
client.open(ClientTimestamp::now()),
Err(ClientError::OpenTimeout)
));
assert!(matches!(client.open(), Err(ClientError::OpenTimeout)));
let observations = server.observations(1);
assert!(matches!(
@@ -185,7 +178,7 @@ fn bad_hmac_echo_reply_is_rejected_without_echo_reply_event() {
};
let mut client = Client::connect(config).unwrap();
let outcome = client.open(ClientTimestamp::now()).unwrap();
let outcome = client.open().unwrap();
assert_started(outcome, &params);
let sent = client.send_probe().unwrap();
@@ -229,7 +222,7 @@ fn hmac_open_reply_with_bad_hmac_fails_with_protocol_error() {
let mut client = Client::connect(config).unwrap();
assert!(matches!(
client.open(ClientTimestamp::now()),
client.open(),
Err(ClientError::Protocol(ProtoError::BadHmac))
));
server.join();
@@ -247,7 +240,7 @@ fn non_hmac_client_open_does_not_set_hmac_flag() {
config.hmac_key = None;
let mut client = Client::connect(config).unwrap();
let outcome = client.open(ClientTimestamp::now()).unwrap();
let outcome = client.open().unwrap();
assert_started(outcome, &params);
let observations = server.observations(1);
@@ -304,7 +297,7 @@ fn backend_hmac_correct_key_succeeds() {
config.hmac_key = Some(key);
let mut client = Client::connect(config).unwrap();
let outcome = client.open(ClientTimestamp::now()).unwrap();
let outcome = client.open().unwrap();
assert!(matches!(outcome, OpenOutcome::Started { .. }));
let sent = client.send_probe().unwrap();
@@ -314,7 +307,7 @@ fn backend_hmac_correct_key_succeeds() {
assert_eq!(events.len(), 1);
assert!(matches!(events[0], ClientEvent::EchoReply { .. }));
client.close(ClientTimestamp::now()).unwrap();
client.close().unwrap();
}
#[test]
@@ -326,10 +319,7 @@ fn backend_hmac_wrong_key_fails() {
config.hmac_key = Some(b"wrong-secret".to_vec());
let mut client = Client::connect(config).unwrap();
assert!(matches!(
client.open(ClientTimestamp::now()),
Err(ClientError::OpenTimeout)
));
assert!(matches!(client.open(), Err(ClientError::OpenTimeout)));
}
#[test]
@@ -341,8 +331,5 @@ fn backend_hmac_missing_key_fails() {
config.hmac_key = None;
let mut client = Client::connect(config).unwrap();
assert!(matches!(
client.open(ClientTimestamp::now()),
Err(ClientError::OpenTimeout)
));
assert!(matches!(client.open(), Err(ClientError::OpenTimeout)));
}
+14 -12
View File
@@ -1,10 +1,12 @@
mod support;
use irtt_client::{Client, ClientConfig, ClientEvent, ClientTimestamp, NegotiationPolicy};
use irtt_proto::{echo_header_len, echo_packet_len, Params, TimestampFields};
use irtt_client::{Client, ClientConfig, ClientEvent, NegotiationPolicy};
use irtt_proto::{echo_header_len, Params, TimestampFields};
use support::{config_for_params, default_params, run_one_probe, BackendPeer, ServerObservation};
use crate::support::test_echo_packet_len;
fn params_with_length(length: i64) -> Params {
Params {
length,
@@ -42,7 +44,7 @@ fn assert_reply_bytes(event: &ClientEvent, expected: usize) {
#[test]
fn length_zero_baseline() {
let params = params_with_length(0);
let expected = echo_packet_len(false, &params);
let expected = test_echo_packet_len(false, &params);
let header = echo_header_len(false, &params);
assert_eq!(expected, header);
}
@@ -50,21 +52,21 @@ fn length_zero_baseline() {
#[test]
fn length_one_accepted() {
let params = params_with_length(1);
let expected = echo_packet_len(false, &params);
let expected = test_echo_packet_len(false, &params);
assert!(expected >= 1);
}
#[test]
fn length_1472_accepted() {
let params = params_with_length(1472);
let expected = echo_packet_len(false, &params);
let expected = test_echo_packet_len(false, &params);
assert_eq!(expected, 1472);
}
#[test]
fn length_4096_accepted() {
let params = params_with_length(4096);
let expected = echo_packet_len(false, &params);
let expected = test_echo_packet_len(false, &params);
assert_eq!(expected, 4096);
}
@@ -74,7 +76,7 @@ fn length_4096_accepted() {
fn echo_request_size_matches_echo_packet_len_for_zero_length() {
let params = params_with_length(0);
let run = run_one_probe(params.clone(), TimestampFields::default());
let expected = echo_packet_len(false, &params);
let expected = test_echo_packet_len(false, &params);
assert_echo_size(&run, expected);
assert_sent_bytes(&run.sent, expected);
}
@@ -84,7 +86,7 @@ fn echo_request_size_matches_echo_packet_len_above_header() {
for length in [16, 60, 128, 1472, 4096] {
let params = params_with_length(length);
let run = run_one_probe(params.clone(), TimestampFields::default());
let expected = echo_packet_len(false, &params);
let expected = test_echo_packet_len(false, &params);
assert_eq!(
expected,
echo_header_len(false, &params).max(length as usize)
@@ -100,7 +102,7 @@ fn echo_request_size_matches_echo_packet_len_above_header() {
fn echo_reply_decoded_for_large_length() {
let params = params_with_length(4096);
let run = run_one_probe(params.clone(), TimestampFields::default());
let expected = echo_packet_len(false, &params);
let expected = test_echo_packet_len(false, &params);
assert_reply(&run.reply);
assert_reply_bytes(&run.reply, expected);
}
@@ -110,7 +112,7 @@ fn echo_reply_bytes_consistent_with_echo_packet_len_for_various_lengths() {
for length in [0, 1472, 4096] {
let params = params_with_length(length);
let run = run_one_probe(params.clone(), TimestampFields::default());
let expected = echo_packet_len(false, &params);
let expected = test_echo_packet_len(false, &params);
assert_reply_bytes(&run.reply, expected);
}
}
@@ -128,7 +130,7 @@ fn backend_large_packet_smoke() {
})
.unwrap();
let outcome = client.open(ClientTimestamp::now()).unwrap();
let outcome = client.open().unwrap();
assert!(matches!(outcome, irtt_client::OpenOutcome::Started { .. }));
client.send_probe().unwrap();
@@ -136,5 +138,5 @@ fn backend_large_packet_smoke() {
assert_eq!(events.len(), 1);
assert!(matches!(events[0], ClientEvent::EchoReply { .. }));
client.close(ClientTimestamp::now()).unwrap();
client.close().unwrap();
}
+22 -31
View File
@@ -1,13 +1,12 @@
mod support;
use irtt_client::ClientTimestamp;
use std::time::Duration;
use irtt_client::{
Client, ClientConfig, ClientError, ClientEvent, OneWayDelaySample, ReceivedStatsSample,
RttSample, ServerTiming,
};
use irtt_proto::{echo_packet_len, Clock, Params, ReceivedStats, StampAt, TimestampFields};
use irtt_proto::{Clock, Params, ReceivedStats, StampAt, TimestampFields};
use support::{
config_for_params, default_params, params_for_modes, run_one_probe, run_one_probe_with_config,
@@ -15,9 +14,10 @@ use support::{
ServerObservation, RECV_COUNT, RECV_WINDOW, TOKEN,
};
use crate::support::test_echo_packet_len;
struct ReplyView<'a> {
seq: u32,
logical_seq: u64,
rtt: &'a RttSample,
server_timing: Option<&'a ServerTiming>,
one_way: Option<&'a OneWayDelaySample>,
@@ -39,8 +39,7 @@ fn received_stats_modes_drive_negotiated_echo_and_reply_events() {
let reply = expect_echo_reply(&run.reply);
assert_eq!(reply.seq, 0);
assert_eq!(reply.logical_seq, 0);
assert_eq!(reply.bytes, echo_packet_len(false, &params));
assert_eq!(reply.bytes, test_echo_packet_len(false, &params));
assert!(reply.server_timing.is_none());
assert!(reply.one_way.is_none());
assert_received_stats(reply.received_stats, expected_count, expected_window);
@@ -149,7 +148,7 @@ fn hmac_rich_mode_uses_negotiated_echo_layout_and_decodes_reply() {
assert_negotiated_echo_use(&run, &params, true);
let reply = expect_echo_reply(&run.reply);
assert_eq!(reply.bytes, echo_packet_len(true, &params));
assert_eq!(reply.bytes, test_echo_packet_len(true, &params));
assert_received_stats(reply.received_stats, Some(RECV_COUNT), Some(RECV_WINDOW));
assert_timing_fields(reply.server_timing, true, true, false, false, true, true);
}
@@ -167,7 +166,7 @@ fn strict_open_rejects_changed_compatibility_params_from_server() {
config.server_addr = server.addr.to_string();
let mut client = Client::connect(config).unwrap();
assert!(matches!(
client.open(irtt_client::ClientTimestamp::now()),
client.open(),
Err(ClientError::NegotiationRejected { .. })
));
server.join();
@@ -186,7 +185,7 @@ fn loose_open_uses_returned_params_for_echo_layout_and_reply_parsing() {
returned.received_stats = ReceivedStats::Count;
returned.stamp_at = StampAt::Receive;
returned.clock = Clock::Wall;
returned.dscp = 8;
returned.dscp = 0;
returned.server_fill = None;
let requested_for_config = requested.clone();
@@ -246,21 +245,15 @@ fn assert_negotiated_echo_use(run: &OneProbeRun, params: &Params, hmac: bool) {
fn assert_echo_uses_params(run: &OneProbeRun, params: &Params, hmac: bool) {
let (echo_len, echo_hmac, token, sequence) = echo_observation(run);
assert_eq!(echo_len, echo_packet_len(hmac, params));
assert_eq!(echo_len, test_echo_packet_len(hmac, params));
assert_eq!(echo_hmac, hmac);
assert_eq!(token, TOKEN);
assert_eq!(sequence, 0);
match &run.sent {
ClientEvent::EchoSent {
seq,
logical_seq,
bytes,
..
} => {
ClientEvent::EchoSent { seq, bytes, .. } => {
assert_eq!(*seq, 0);
assert_eq!(*logical_seq, 0);
assert_eq!(*bytes, echo_packet_len(hmac, params));
assert_eq!(*bytes, test_echo_packet_len(hmac, params));
}
other => panic!("expected EchoSent, got {other:?}"),
}
@@ -289,7 +282,6 @@ fn expect_echo_reply(event: &ClientEvent) -> ReplyView<'_> {
match event {
ClientEvent::EchoReply {
seq,
logical_seq,
rtt,
server_timing,
one_way,
@@ -298,7 +290,6 @@ fn expect_echo_reply(event: &ClientEvent) -> ReplyView<'_> {
..
} => ReplyView {
seq: *seq,
logical_seq: *logical_seq,
rtt,
server_timing: server_timing.as_ref(),
one_way: one_way.as_ref(),
@@ -379,7 +370,7 @@ fn backend_basic_open_echo_close() {
let peer = BackendPeer::start_open_echo(params.clone(), None);
let mut client = Client::connect(config_for_params(peer.addr(), &params)).unwrap();
let outcome = client.open(ClientTimestamp::now()).unwrap();
let outcome = client.open().unwrap();
assert!(matches!(outcome, irtt_client::OpenOutcome::Started { .. }));
let sent = client.send_probe().unwrap();
@@ -389,7 +380,7 @@ fn backend_basic_open_echo_close() {
assert_eq!(events.len(), 1);
assert!(matches!(events[0], ClientEvent::EchoReply { .. }));
client.close(ClientTimestamp::now()).unwrap();
client.close().unwrap();
}
#[test]
@@ -400,7 +391,7 @@ fn backend_ttl_smoke() {
config.socket_config.ttl = Some(64);
let mut client = Client::connect(config).unwrap();
let outcome = client.open(ClientTimestamp::now()).unwrap();
let outcome = client.open().unwrap();
assert!(matches!(outcome, irtt_client::OpenOutcome::Started { .. }));
let sent = client.send_probe().unwrap();
@@ -410,7 +401,7 @@ fn backend_ttl_smoke() {
assert_eq!(events.len(), 1);
assert!(matches!(events[0], ClientEvent::EchoReply { .. }));
client.close(ClientTimestamp::now()).unwrap();
client.close().unwrap();
}
#[test]
@@ -420,9 +411,9 @@ fn dscp_configured_open_close_smoke() {
let server = start_open_server(params.clone(), None);
let mut client = Client::connect(config_for_params(server.addr, &params)).unwrap();
let outcome = client.open(ClientTimestamp::now()).unwrap();
let outcome = client.open().unwrap();
assert!(matches!(outcome, irtt_client::OpenOutcome::Started { .. }));
client.close(ClientTimestamp::now()).unwrap();
client.close().unwrap();
server.join();
}
@@ -438,13 +429,13 @@ fn backend_received_stats_smoke() {
let peer = BackendPeer::start_open_echo(params.clone(), None);
let mut client = Client::connect(config_for_params(peer.addr(), &params)).unwrap();
client.open(ClientTimestamp::now()).unwrap();
client.open().unwrap();
client.send_probe().unwrap();
let events = client.recv_once().unwrap();
assert_eq!(events.len(), 1);
assert!(matches!(events[0], ClientEvent::EchoReply { .. }));
client.close(ClientTimestamp::now()).unwrap();
client.close().unwrap();
}
}
@@ -461,7 +452,7 @@ fn backend_timestamp_smoke() {
let peer = BackendPeer::start_open_echo(params.clone(), None);
let mut client = Client::connect(config_for_params(peer.addr(), &params)).unwrap();
client.open(ClientTimestamp::now()).unwrap();
client.open().unwrap();
client.send_probe().unwrap();
let events = client.recv_once().unwrap();
assert_eq!(events.len(), 1);
@@ -497,7 +488,7 @@ fn backend_timestamp_smoke() {
other => panic!("expected EchoReply, got {other:?}"),
}
client.close(ClientTimestamp::now()).unwrap();
client.close().unwrap();
}
}
@@ -508,7 +499,7 @@ fn backend_clock_smoke() {
let peer = BackendPeer::start_open_echo(params.clone(), None);
let mut client = Client::connect(config_for_params(peer.addr(), &params)).unwrap();
client.open(ClientTimestamp::now()).unwrap();
client.open().unwrap();
client.send_probe().unwrap();
let events = client.recv_once().unwrap();
assert_eq!(events.len(), 1);
@@ -536,6 +527,6 @@ fn backend_clock_smoke() {
other => panic!("expected EchoReply, got {other:?}"),
}
client.close(ClientTimestamp::now()).unwrap();
client.close().unwrap();
}
}
+9 -9
View File
@@ -1,15 +1,15 @@
mod support;
use irtt_client::{
Client, ClientConfig, ClientError, ClientEvent, ClientTimestamp, NegotiationPolicy,
};
use irtt_proto::{echo_packet_len, Params, TimestampFields};
use irtt_client::{Client, ClientConfig, ClientError, ClientEvent, NegotiationPolicy};
use irtt_proto::{Params, TimestampFields};
use support::{
config_for_params, default_params, run_one_probe, run_one_probe_with_config, server_fill,
start_open_server, OneProbeRun, ServerObservation,
};
use crate::support::test_echo_packet_len;
fn open_observation_params(run: &OneProbeRun) -> &Params {
match &run.observations[0] {
ServerObservation::Open { params, .. } => params,
@@ -112,7 +112,7 @@ fn server_fill_strict_rejects_removed_fill() {
let server = start_open_server(returned, None);
let mut client = Client::connect(config_for_params(server.addr, &requested)).unwrap();
assert!(matches!(
client.open(ClientTimestamp::now()),
client.open(),
Err(ClientError::NegotiationRejected { .. })
));
server.join();
@@ -129,7 +129,7 @@ fn server_fill_strict_rejects_different_fill_value() {
let server = start_open_server(returned, None);
let mut client = Client::connect(config_for_params(server.addr, &requested)).unwrap();
assert!(matches!(
client.open(ClientTimestamp::now()),
client.open(),
Err(ClientError::NegotiationRejected { .. })
));
server.join();
@@ -145,7 +145,7 @@ fn server_fill_strict_rejects_unexpected_fill_from_server() {
let server = start_open_server(returned, None);
let mut client = Client::connect(config_for_params(server.addr, &requested)).unwrap();
assert!(matches!(
client.open(ClientTimestamp::now()),
client.open(),
Err(ClientError::NegotiationRejected { .. })
));
server.join();
@@ -204,11 +204,11 @@ fn server_fill_does_not_affect_client_side_packet_layout() {
let (echo_len_without, _, _, _) = echo_observation(&run_without);
assert_eq!(echo_len_with, echo_len_without);
assert_eq!(
echo_packet_len(false, &run_with.negotiated.params),
test_echo_packet_len(false, &run_with.negotiated.params),
echo_len_with
);
assert_eq!(
echo_packet_len(false, &run_without.negotiated.params),
test_echo_packet_len(false, &run_without.negotiated.params),
echo_len_without
);
}
+8 -5
View File
@@ -10,9 +10,7 @@ use std::{
time::{Duration, SystemTime},
};
use irtt_client::{
Client, ClientConfig, ClientEvent, ClientTimestamp, NegotiatedParams, OpenOutcome, SocketConfig,
};
use irtt_client::{Client, ClientConfig, ClientEvent, NegotiatedParams, OpenOutcome, SocketConfig};
use irtt_proto::{
compute_hmac_in_place, echo_packet_len, flags, verify_hmac, Clock, Params, ProtoError,
ReceivedStats, ServerFill, StampAt, TimestampFields, HMAC_SIZE, MAGIC, PROTOCOL_VERSION,
@@ -216,7 +214,7 @@ where
{
let server = start_one_probe_server(server_params, timestamps, hmac_key);
let mut client = Client::connect(build_config(server.addr)).unwrap();
let negotiated = assert_started(client.open(ClientTimestamp::now()).unwrap());
let negotiated = assert_started(client.open().unwrap());
let sent_events = client.send_probe().unwrap();
assert_eq!(sent_events.len(), 1);
@@ -522,6 +520,11 @@ fn open_reply(flags: u8, token: u64, params: &Params, hmac_key: Option<&[u8]>) -
packet
}
pub(super) fn test_echo_packet_len(hmac: bool, params: &Params) -> usize {
echo_packet_len(hmac, params)
.expect("managed runner test params must have a non-negative packet length")
}
fn echo_reply_packet(
token: u64,
seq: u32,
@@ -531,7 +534,7 @@ fn echo_reply_packet(
) -> Vec<u8> {
let has_hmac = hmac_key.is_some();
let layout = irtt_proto::PacketLayout::echo(has_hmac, params);
let packet_len = echo_packet_len(has_hmac, params);
let packet_len = test_echo_packet_len(has_hmac, params);
let mut packet = Vec::with_capacity(packet_len);
packet.extend_from_slice(&MAGIC);
+5 -5
View File
@@ -1,6 +1,6 @@
[package]
name = "irtt-proto"
version = "0.1.0"
version = "0.2.0"
description = "IRTT protocol encoding and decoding primitives"
edition.workspace = true
license.workspace = true
@@ -12,7 +12,7 @@ keywords = ["irtt", "networking", "latency", "protocol"]
categories = ["network-programming"]
[dependencies]
hmac = "0.12"
md-5 = "0.10"
subtle = "2"
thiserror = "2"
hmac = "0.13"
md-5 = "0.11"
subtle = "2.6.1"
thiserror = "2.0.18"
+4 -4
View File
@@ -1,7 +1,7 @@
use crate::{
flags::{has, FLAG_HMAC, FLAG_OPEN, FLAG_REPLY},
hmac,
layout::{try_echo_packet_len, PacketLayout},
layout::{echo_packet_len, PacketLayout},
open::{check_hmac_presence, reject},
params::Params,
validate_header, write_header, ProtoError, Result, HEADER_SIZE, HMAC_SIZE, RECV_COUNT_SIZE,
@@ -43,7 +43,7 @@ pub fn encode_echo_request(request: &EchoRequest, hmac_key: Option<&[u8]>) -> Re
flags |= FLAG_HMAC;
}
let layout = PacketLayout::echo(hmac_key.is_some(), &request.params);
let len = try_echo_packet_len(hmac_key.is_some(), &request.params)?;
let len = echo_packet_len(hmac_key.is_some(), &request.params)?;
let payload_offset = layout.header_len();
let available_payload_len = len.saturating_sub(payload_offset);
if request.payload.len() > available_payload_len {
@@ -87,7 +87,7 @@ pub fn decode_echo_reply(
actual: packet.len(),
});
}
let expected_len = try_echo_packet_len(has(flags, FLAG_HMAC), params)?;
let expected_len = echo_packet_len(has(flags, FLAG_HMAC), params)?;
if packet.len() != expected_len {
return Err(ProtoError::PacketLengthMismatch {
expected: expected_len,
@@ -371,7 +371,7 @@ mod tests {
};
params.received_stats = ReceivedStats::Both;
let layout = PacketLayout::echo(true, &params);
let mut packet = Vec::with_capacity(try_echo_packet_len(true, &params).unwrap());
let mut packet = Vec::with_capacity(echo_packet_len(true, &params).unwrap());
write_header(&mut packet, FLAG_REPLY | FLAG_HMAC);
packet.extend_from_slice(&[0; HMAC_SIZE]);
packet.extend_from_slice(&0x7896_b6ab_8771_5213u64.to_le_bytes());
+1 -1
View File
@@ -1,4 +1,4 @@
use hmac::{Hmac, Mac};
use hmac::{Hmac, KeyInit, Mac};
use md5::Md5;
use subtle::ConstantTimeEq;
+6 -12
View File
@@ -111,13 +111,7 @@ pub fn echo_header_len(hmac: bool, params: &Params) -> usize {
PacketLayout::echo(hmac, params).header_len()
}
pub fn echo_packet_len(hmac: bool, params: &Params) -> usize {
let header_len = echo_header_len(hmac, params);
let requested = usize::try_from(params.length).unwrap_or(0);
header_len.max(requested)
}
pub fn try_echo_packet_len(hmac: bool, params: &Params) -> Result<usize> {
pub fn echo_packet_len(hmac: bool, params: &Params) -> Result<usize> {
let header_len = echo_header_len(hmac, params);
let requested =
usize::try_from(params.length).map_err(|_| ProtoError::NegativePacketLength {
@@ -300,9 +294,9 @@ mod tests {
fn negotiated_length_never_smaller_than_header() {
let mut p = params(ReceivedStats::Both, StampAt::Both, Clock::Both);
p.length = 20;
assert_eq!(echo_packet_len(false, &p), 60);
assert_eq!(echo_packet_len(false, &p).unwrap(), 60);
p.length = 92;
assert_eq!(echo_packet_len(false, &p), 92);
assert_eq!(echo_packet_len(false, &p).unwrap(), 92);
}
#[test]
@@ -311,7 +305,7 @@ mod tests {
p.length = -1;
assert_eq!(
try_echo_packet_len(false, &p),
echo_packet_len(false, &p),
Err(ProtoError::NegativePacketLength { length: -1 })
);
}
@@ -320,9 +314,9 @@ mod tests {
fn checked_packet_len_preserves_non_negative_header_floor() {
let mut p = params(ReceivedStats::Both, StampAt::Both, Clock::Both);
p.length = 0;
assert_eq!(try_echo_packet_len(false, &p), Ok(60));
assert_eq!(echo_packet_len(false, &p), Ok(60));
p.length = 92;
assert_eq!(try_echo_packet_len(false, &p), Ok(92));
assert_eq!(echo_packet_len(false, &p), Ok(92));
}
}
+1 -1
View File
@@ -15,7 +15,7 @@ pub use echo::{decode_echo_reply, encode_echo_request, EchoReply, EchoRequest, T
pub use error::{ProtoError, Result};
pub use flags::*;
pub use hmac::{compute_hmac, compute_hmac_in_place, verify_hmac};
pub use layout::{echo_header_len, echo_packet_len, try_echo_packet_len, PacketLayout};
pub use layout::{echo_header_len, echo_packet_len, PacketLayout};
pub use open::{decode_open_reply, encode_open_request, OpenReply, OpenRequest};
pub use params::{Clock, Params, ReceivedStats, ServerFill, StampAt};
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "irtt-stats"
version = "0.1.0"
version = "0.2.0"
description = "Statistics aggregation for irtt-client events"
edition.workspace = true
license.workspace = true
+249
View File
@@ -0,0 +1,249 @@
use std::time::Duration;
use crate::ipdv::{IpdvSample, IpdvTracker};
use crate::loss::loss_stats;
use crate::normalization::{ReplySample, StatsEvent};
use crate::time_stats::TimeMetric;
use crate::{
EventCounts, EventStatsUpdate, IpdvPairUpdate, IpdvStats, OneWayDelayStats, PacketCounts,
RttStats, SampleMode, ServerProcessingStats, Snapshot,
};
const CONTINUOUS_SEQUENCE_LIMIT: usize = 4096;
#[derive(Debug, Clone, PartialEq)]
pub(crate) struct CoreStats {
events: EventCounts,
packets: PacketCounts,
send_call: TimeMetric,
timer_error: TimeMetric,
rtt_primary: TimeMetric,
rtt_raw: TimeMetric,
rtt_adjusted: TimeMetric,
ipdv_round_trip: TimeMetric,
ipdv_send: TimeMetric,
ipdv_receive: TimeMetric,
send_delay: TimeMetric,
receive_delay: TimeMetric,
server_processing: TimeMetric,
ipdv_tracker: IpdvTracker,
}
impl CoreStats {
pub(crate) fn new(sample_mode: SampleMode) -> Self {
let sequence_limit = if sample_mode == SampleMode::Exact {
None
} else {
Some(CONTINUOUS_SEQUENCE_LIMIT)
};
Self {
events: EventCounts::default(),
packets: PacketCounts::default(),
send_call: TimeMetric::new(false),
timer_error: TimeMetric::new(false),
rtt_primary: TimeMetric::new(sample_mode == SampleMode::Exact),
rtt_raw: TimeMetric::new(sample_mode == SampleMode::Exact),
rtt_adjusted: TimeMetric::new(sample_mode == SampleMode::Exact),
ipdv_round_trip: TimeMetric::new(sample_mode == SampleMode::Exact),
ipdv_send: TimeMetric::new(sample_mode == SampleMode::Exact),
ipdv_receive: TimeMetric::new(sample_mode == SampleMode::Exact),
send_delay: TimeMetric::new(sample_mode == SampleMode::Exact),
receive_delay: TimeMetric::new(sample_mode == SampleMode::Exact),
server_processing: TimeMetric::new(false),
ipdv_tracker: IpdvTracker::new(sequence_limit),
}
}
pub(crate) fn apply(&mut self, event: StatsEvent) -> EventStatsUpdate {
match event {
StatsEvent::Sent {
bytes,
send_call_ns,
timer_error_ns,
..
} => {
self.apply_sent(bytes, send_call_ns, timer_error_ns);
EventStatsUpdate::default()
}
StatsEvent::UniqueReply {
is_late, sample, ..
} => self.apply_unique_reply(is_late, *sample),
StatsEvent::DuplicateReply { .. } => {
self.apply_duplicate_reply();
EventStatsUpdate::default()
}
StatsEvent::Loss { .. } => {
self.apply_loss();
EventStatsUpdate::default()
}
StatsEvent::Warning { .. } => {
self.apply_warning();
EventStatsUpdate::default()
}
StatsEvent::UntrackedLate { .. } => {
self.apply_untracked_late();
EventStatsUpdate::default()
}
}
}
fn apply_sent(&mut self, bytes: usize, send_call_ns: i128, timer_error_ns: i128) {
self.events.sent_events += 1;
self.packets.packets_sent += 1;
self.packets.bytes_sent = self.packets.bytes_sent.saturating_add(bytes as u64);
self.send_call.push_ns(send_call_ns);
self.timer_error.push_ns(timer_error_ns);
}
fn apply_unique_reply(&mut self, is_late: bool, sample: ReplySample) -> EventStatsUpdate {
let mut update = EventStatsUpdate {
contributed_sample: true,
..EventStatsUpdate::default()
};
self.account_unique_reply(is_late, &sample);
self.record_reply_metrics(&sample);
update.ipdv_pairs = self.apply_ipdv_sample(sample.ipdv);
update
}
fn account_unique_reply(&mut self, is_late: bool, sample: &ReplySample) {
self.events.echo_replies += u64::from(!is_late);
self.events.late_unique_replies += u64::from(is_late);
self.packets.packets_received += 1;
self.packets.unique_replies += 1;
self.packets.late_packets += u64::from(is_late);
self.packets.bytes_received = self
.packets
.bytes_received
.saturating_add(sample.bytes as u64);
if let Some(count) = sample.received_count {
self.packets.server_packets_received = Some(u64::from(count));
}
if let Some(window) = sample.received_window {
self.packets.server_received_window = Some(window);
}
}
fn record_reply_metrics(&mut self, sample: &ReplySample) {
self.rtt_primary.push_ns(sample.ipdv.rtt_primary_ns);
self.rtt_raw.push_ns(sample.rtt_raw_ns);
if let Some(adjusted) = sample.rtt_adjusted_ns {
self.rtt_adjusted.push_ns(adjusted);
}
if let Some(processing) = sample.server_processing_ns {
self.server_processing.push_ns(processing);
}
if let Some(delay) = sample.send_delay_ns {
self.send_delay.push_ns(delay);
}
if let Some(delay) = sample.receive_delay_ns {
self.receive_delay.push_ns(delay);
}
}
fn apply_ipdv_sample(&mut self, sample: IpdvSample) -> Vec<IpdvPairUpdate> {
let mut updates = Vec::new();
for pair in self.ipdv_tracker.insert(sample) {
let Some(rtt_ipdv) = duration_from_non_negative_i128_ns(pair.rtt_ipdv_ns) else {
continue;
};
let send_ipdv = pair
.send_ipdv_ns
.and_then(duration_from_non_negative_i128_ns);
let receive_ipdv = pair
.receive_ipdv_ns
.and_then(duration_from_non_negative_i128_ns);
self.ipdv_round_trip.push_ns(pair.rtt_ipdv_ns);
if let Some(value) = pair.send_ipdv_ns {
self.ipdv_send.push_ns(value);
}
if let Some(value) = pair.receive_ipdv_ns {
self.ipdv_receive.push_ns(value);
}
updates.push(IpdvPairUpdate {
previous_seq: pair.previous_seq,
current_seq: pair.current_seq,
rtt_ipdv,
send_ipdv,
receive_ipdv,
});
}
updates
}
fn apply_duplicate_reply(&mut self) {
self.events.duplicate_replies += 1;
self.packets.packets_received += 1;
self.packets.duplicates += 1;
}
fn apply_loss(&mut self) {
self.events.loss_events += 1;
}
fn apply_warning(&mut self) {
self.events.warning_events += 1;
}
fn apply_untracked_late(&mut self) {
self.events.untracked_late_replies += 1;
}
pub(crate) fn snapshot(&self) -> Snapshot {
let packets = self.packets;
Snapshot {
events: self.events,
packets,
loss: loss_stats(packets),
send_call: self.send_call.stats(),
timer_error: self.timer_error.stats(),
rtt: self.rtt_stats(),
ipdv: self.ipdv_stats(),
one_way_delay: self.one_way_delay_stats(),
server_processing: self.server_processing_stats(),
}
}
fn rtt_stats(&self) -> RttStats {
RttStats {
primary: self.rtt_primary.stats(),
raw: self.rtt_raw.stats(),
adjusted: self.rtt_adjusted.stats(),
}
}
fn ipdv_stats(&self) -> IpdvStats {
IpdvStats {
round_trip: self.ipdv_round_trip.stats(),
send: self.ipdv_send.stats(),
receive: self.ipdv_receive.stats(),
}
}
fn one_way_delay_stats(&self) -> OneWayDelayStats {
OneWayDelayStats {
send_delay: self.send_delay.stats(),
receive_delay: self.receive_delay.stats(),
}
}
fn server_processing_stats(&self) -> ServerProcessingStats {
ServerProcessingStats {
processing: self.server_processing.stats(),
}
}
}
fn duration_from_non_negative_i128_ns(value: i128) -> Option<Duration> {
u64::try_from(value).ok().map(Duration::from_nanos)
}
+263
View File
@@ -0,0 +1,263 @@
use std::{
collections::{HashMap, HashSet, VecDeque},
time::{Duration, Instant},
};
#[derive(Debug, Clone, PartialEq)]
pub(crate) struct IpdvSample {
pub(crate) seq: u32,
pub(crate) rtt_primary_ns: i128,
pub(crate) client_send_mono: Instant,
pub(crate) client_receive_mono: Instant,
pub(crate) client_send_wall_ns: Option<i128>,
pub(crate) client_receive_wall_ns: Option<i128>,
pub(crate) server_receive_mono_ns: Option<i64>,
pub(crate) server_send_mono_ns: Option<i64>,
pub(crate) server_receive_wall_ns: Option<i64>,
pub(crate) server_send_wall_ns: Option<i64>,
}
#[derive(Debug, Clone, PartialEq)]
pub(crate) struct IpdvTracker {
samples: HashMap<u32, IpdvSample>,
sample_order: VecDeque<u32>,
completed_pairs: HashSet<u32>,
sequence_limit: Option<usize>,
}
impl IpdvTracker {
pub(crate) fn new(sequence_limit: Option<usize>) -> Self {
Self {
samples: HashMap::new(),
sample_order: VecDeque::new(),
completed_pairs: HashSet::new(),
sequence_limit,
}
}
pub(crate) fn insert(&mut self, sample: IpdvSample) -> Vec<CompletedIpdvPair> {
let seq = sample.seq;
if self.samples.insert(seq, sample).is_some() {
return Vec::new();
}
self.sample_order.push_back(seq);
self.enforce_sequence_limit();
let mut pairs = Vec::with_capacity(2);
if let Some(pair) = self.try_pair(seq) {
pairs.push(pair);
}
if let Some(next) = seq.checked_add(1) {
if let Some(pair) = self.try_pair(next) {
pairs.push(pair);
}
}
pairs
}
fn enforce_sequence_limit(&mut self) {
let Some(limit) = self.sequence_limit else {
return;
};
while self.samples.len() > limit {
let Some(seq) = self.sample_order.pop_front() else {
break;
};
if self.samples.remove(&seq).is_some() {
self.completed_pairs.remove(&seq);
if let Some(next) = seq.checked_add(1) {
self.completed_pairs.remove(&next);
}
}
}
}
fn try_pair(&mut self, current_seq: u32) -> Option<CompletedIpdvPair> {
let previous_seq = current_seq.checked_sub(1)?;
if !self.completed_pairs.insert(current_seq) {
return None;
}
let Some(previous) = self.samples.get(&previous_seq) else {
self.completed_pairs.remove(&current_seq);
return None;
};
let Some(current) = self.samples.get(&current_seq) else {
self.completed_pairs.remove(&current_seq);
return None;
};
Some(CompletedIpdvPair {
previous_seq,
current_seq,
rtt_ipdv_ns: abs_i128_ns(current.rtt_primary_ns - previous.rtt_primary_ns),
send_ipdv_ns: send_ipdv_ns(previous, current).map(abs_i128_ns),
receive_ipdv_ns: receive_ipdv_ns(previous, current).map(abs_i128_ns),
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct CompletedIpdvPair {
pub(crate) previous_seq: u32,
pub(crate) current_seq: u32,
pub(crate) rtt_ipdv_ns: i128,
pub(crate) send_ipdv_ns: Option<i128>,
pub(crate) receive_ipdv_ns: Option<i128>,
}
fn send_ipdv_ns(previous: &IpdvSample, current: &IpdvSample) -> Option<i128> {
if let (Some(prev_server), Some(cur_server)) = (
previous.server_receive_mono_ns,
current.server_receive_mono_ns,
) {
return Some(
i128::from(cur_server)
- i128::from(prev_server)
- instant_diff_ns(current.client_send_mono, previous.client_send_mono),
);
}
if let (Some(prev_server), Some(cur_server), Some(prev_client), Some(cur_client)) = (
previous.server_receive_wall_ns,
current.server_receive_wall_ns,
previous.client_send_wall_ns,
current.client_send_wall_ns,
) {
return Some(i128::from(cur_server) - i128::from(prev_server) - (cur_client - prev_client));
}
None
}
fn receive_ipdv_ns(previous: &IpdvSample, current: &IpdvSample) -> Option<i128> {
if let (Some(prev_server), Some(cur_server)) =
(previous.server_send_mono_ns, current.server_send_mono_ns)
{
return Some(
instant_diff_ns(current.client_receive_mono, previous.client_receive_mono)
- (i128::from(cur_server) - i128::from(prev_server)),
);
}
if let (Some(prev_server), Some(cur_server), Some(prev_client), Some(cur_client)) = (
previous.server_send_wall_ns,
current.server_send_wall_ns,
previous.client_receive_wall_ns,
current.client_receive_wall_ns,
) {
return Some(
(cur_client - prev_client) - (i128::from(cur_server) - i128::from(prev_server)),
);
}
None
}
fn instant_diff_ns(current: Instant, previous: Instant) -> i128 {
if let Some(diff) = current.checked_duration_since(previous) {
duration_ns_i128(diff)
} else {
-duration_ns_i128(previous.duration_since(current))
}
}
fn duration_ns_i128(duration: Duration) -> i128 {
i128::try_from(duration.as_nanos()).unwrap_or(i128::MAX)
}
fn abs_i128_ns(value: i128) -> i128 {
value.saturating_abs()
}
#[cfg(test)]
mod tests {
use super::*;
fn ipdv_sample(seq: u32, rtt_primary_ns: i128) -> IpdvSample {
let now = Instant::now();
IpdvSample {
seq,
rtt_primary_ns,
client_send_mono: now,
client_receive_mono: now,
client_send_wall_ns: None,
client_receive_wall_ns: None,
server_receive_mono_ns: None,
server_send_mono_ns: None,
server_receive_wall_ns: None,
server_send_wall_ns: None,
}
}
#[test]
fn ipdv_tracker_completes_adjacent_pair() {
let mut tracker = IpdvTracker::new(None);
assert!(tracker.insert(ipdv_sample(0, 10)).is_empty());
let pairs = tracker.insert(ipdv_sample(1, 14));
assert_eq!(
pairs,
vec![CompletedIpdvPair {
previous_seq: 0,
current_seq: 1,
rtt_ipdv_ns: 4,
send_ipdv_ns: None,
receive_ipdv_ns: None,
}]
);
}
#[test]
fn ipdv_tracker_gap_fill_completes_both_adjacent_pairs() {
let mut tracker = IpdvTracker::new(None);
assert!(tracker.insert(ipdv_sample(0, 10)).is_empty());
assert!(tracker.insert(ipdv_sample(2, 20)).is_empty());
let pairs = tracker.insert(ipdv_sample(1, 13));
assert_eq!(
pairs,
vec![
CompletedIpdvPair {
previous_seq: 0,
current_seq: 1,
rtt_ipdv_ns: 3,
send_ipdv_ns: None,
receive_ipdv_ns: None,
},
CompletedIpdvPair {
previous_seq: 1,
current_seq: 2,
rtt_ipdv_ns: 7,
send_ipdv_ns: None,
receive_ipdv_ns: None,
},
]
);
}
#[test]
fn ipdv_tracker_duplicate_sequence_does_not_emit_pair_again() {
let mut tracker = IpdvTracker::new(None);
assert!(tracker.insert(ipdv_sample(0, 10)).is_empty());
assert_eq!(tracker.insert(ipdv_sample(1, 14)).len(), 1);
assert!(tracker.insert(ipdv_sample(1, 18)).is_empty());
}
#[test]
fn ipdv_tracker_bounded_mode_evicts_old_sequence_state() {
let mut tracker = IpdvTracker::new(Some(2));
assert!(tracker.insert(ipdv_sample(0, 10)).is_empty());
assert_eq!(tracker.insert(ipdv_sample(1, 14)).len(), 1);
let pairs = tracker.insert(ipdv_sample(2, 20));
assert_eq!(pairs.len(), 1);
assert!(!tracker.samples.contains_key(&0));
assert!(!tracker.completed_pairs.contains(&0));
assert!(!tracker.completed_pairs.contains(&1));
assert!(tracker.completed_pairs.contains(&2));
}
}
+343 -1310
View File
File diff suppressed because it is too large. Load diff
+106
View File
@@ -0,0 +1,106 @@
use crate::PacketCounts;
#[derive(Debug, Clone, Copy, PartialEq)]
/// Packet loss, duplicate, and late-packet statistics.
///
/// Percentage fields are percentages from `0.0` to `100.0` in normal cases.
/// Directional upstream and downstream loss can be negative when
/// server-reported counts exceed local expectations.
pub struct LossStats {
/// Locally inferred total lost packets.
pub lost_packets: u64,
/// Server-assisted upstream loss estimate, when server counts are available.
pub upstream_loss_packets: Option<i128>,
/// Server-assisted downstream loss estimate, when server counts are available.
pub downstream_loss_packets: Option<i128>,
/// Total packet loss percentage.
pub packet_loss_percent: f64,
/// Server-assisted upstream loss percentage.
pub upstream_loss_percent: f64,
/// Server-assisted downstream loss percentage.
pub downstream_loss_percent: f64,
/// Duplicate reply percentage.
pub duplicate_percent: f64,
/// Late reply packet percentage.
pub late_packets_percent: f64,
}
pub(crate) fn loss_stats(packets: PacketCounts) -> LossStats {
let lost = packets.packets_sent.saturating_sub(packets.unique_replies);
let packet_loss_percent = if packets.packets_sent == 0 {
0.0
} else if packets.unique_replies == 0 {
100.0
} else {
percent(lost as f64, packets.packets_sent as f64)
};
let (
upstream_loss_packets,
upstream_loss_percent,
downstream_loss_packets,
downstream_loss_percent,
) = if let Some(server_received) = packets.server_packets_received {
let upstream = i128::from(packets.packets_sent) - i128::from(server_received);
let downstream = i128::from(server_received) - i128::from(packets.packets_received);
let upstream_percent = if packets.packets_sent == 0 {
0.0
} else {
percent(upstream as f64, packets.packets_sent as f64)
};
let downstream_percent = if server_received == 0 {
0.0
} else {
percent(downstream as f64, server_received as f64)
};
(
Some(upstream),
upstream_percent,
Some(downstream),
downstream_percent,
)
} else {
(None, 0.0, None, 0.0)
};
LossStats {
lost_packets: lost,
upstream_loss_packets,
downstream_loss_packets,
packet_loss_percent,
upstream_loss_percent,
downstream_loss_percent,
duplicate_percent: if packets.packets_received == 0 {
0.0
} else {
percent(packets.duplicates as f64, packets.packets_received as f64)
},
late_packets_percent: if packets.packets_received == 0 {
0.0
} else {
percent(packets.late_packets as f64, packets.packets_received as f64)
},
}
}
fn percent(numerator: f64, denominator: f64) -> f64 {
100.0 * numerator / denominator
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn duplicate_and_late_percentages_use_packets_received_denominator() {
let loss = loss_stats(PacketCounts {
packets_received: 4,
duplicates: 1,
late_packets: 2,
..PacketCounts::default()
});
assert_eq!(loss.duplicate_percent, 25.0);
assert_eq!(loss.late_packets_percent, 50.0);
}
}
+214
View File
@@ -0,0 +1,214 @@
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
use irtt_client::{
ClientEvent, ClientTimestamp, OneWayDelaySample, ReceivedStatsSample, RttSample, ServerTiming,
SignedDuration,
};
use crate::ipdv::IpdvSample;
#[derive(Debug, Clone, PartialEq)]
pub(crate) enum StatsEvent {
Sent {
at: Instant,
bytes: usize,
send_call_ns: i128,
timer_error_ns: i128,
},
UniqueReply {
at: Instant,
is_late: bool,
sample: Box<ReplySample>,
},
DuplicateReply {
at: Instant,
},
Loss {
at: Instant,
},
Warning {
at: Instant,
},
UntrackedLate {
at: Instant,
},
}
pub(crate) fn normalize_event(event: &ClientEvent) -> Option<StatsEvent> {
match event {
ClientEvent::EchoSent {
sent_at,
bytes,
send_call,
timer_error,
..
} => Some(StatsEvent::Sent {
at: sent_at.mono,
bytes: *bytes,
send_call_ns: duration_ns_i128(*send_call),
timer_error_ns: duration_ns_i128(*timer_error),
}),
ClientEvent::EchoReply {
seq,
sent_at,
received_at,
rtt,
server_timing,
one_way,
received_stats,
bytes,
..
} => Some(StatsEvent::UniqueReply {
at: received_at.mono,
is_late: false,
sample: Box::new(ReplySample::from_reply_parts(
*seq,
*sent_at,
*received_at,
*rtt,
*server_timing,
*one_way,
*received_stats,
*bytes,
)),
}),
ClientEvent::LateReply {
seq,
sent_at: Some(sent_at),
received_at,
rtt: Some(rtt),
server_timing,
one_way,
received_stats,
bytes,
..
} => Some(StatsEvent::UniqueReply {
at: received_at.mono,
is_late: true,
sample: Box::new(ReplySample::from_reply_parts(
*seq,
*sent_at,
*received_at,
*rtt,
*server_timing,
*one_way,
*received_stats,
*bytes,
)),
}),
ClientEvent::LateReply { received_at, .. } => Some(StatsEvent::UntrackedLate {
at: received_at.mono,
}),
ClientEvent::DuplicateReply { received_at, .. } => Some(StatsEvent::DuplicateReply {
at: received_at.mono,
}),
ClientEvent::EchoLoss { timeout_at, .. } => Some(StatsEvent::Loss { at: *timeout_at }),
ClientEvent::Warning { at, .. } => Some(StatsEvent::Warning { at: at.mono }),
ClientEvent::SessionStarted { .. }
| ClientEvent::NoTestCompleted { .. }
| ClientEvent::SessionClosed { .. } => None,
}
}
impl StatsEvent {
pub(crate) fn at(&self) -> Instant {
match self {
Self::Sent { at, .. }
| Self::UniqueReply { at, .. }
| Self::DuplicateReply { at }
| Self::Loss { at }
| Self::Warning { at }
| Self::UntrackedLate { at } => *at,
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub(crate) struct ReplySample {
pub(crate) bytes: usize,
pub(crate) rtt_raw_ns: i128,
pub(crate) rtt_adjusted_ns: Option<i128>,
pub(crate) send_delay_ns: Option<i128>,
pub(crate) receive_delay_ns: Option<i128>,
pub(crate) server_processing_ns: Option<i128>,
pub(crate) received_count: Option<u32>,
pub(crate) received_window: Option<u64>,
pub(crate) ipdv: IpdvSample,
}
impl ReplySample {
#[allow(clippy::too_many_arguments)]
fn from_reply_parts(
seq: u32,
sent_at: ClientTimestamp,
received_at: ClientTimestamp,
rtt: RttSample,
server_timing: Option<ServerTiming>,
one_way: Option<OneWayDelaySample>,
received_stats: Option<ReceivedStatsSample>,
bytes: usize,
) -> Self {
let rtt_primary_ns = signed_duration_ns(rtt.effective_signed);
Self {
bytes,
rtt_raw_ns: duration_ns_i128(rtt.raw),
rtt_adjusted_ns: rtt.adjusted_signed.map(signed_duration_ns),
send_delay_ns: one_way
.and_then(|sample| sample.client_to_server)
.map(duration_ns_i128),
receive_delay_ns: one_way
.and_then(|sample| sample.server_to_client)
.map(duration_ns_i128),
server_processing_ns: server_timing
.and_then(|timing| timing.processing)
.map(duration_ns_i128),
received_count: received_stats.and_then(|stats| stats.count),
received_window: received_stats.and_then(|stats| stats.window),
ipdv: IpdvSample {
seq,
rtt_primary_ns,
client_send_mono: sent_at.mono,
client_receive_mono: received_at.mono,
client_send_wall_ns: system_time_ns(sent_at.wall),
client_receive_wall_ns: system_time_ns(received_at.wall),
server_receive_mono_ns: server_timing.and_then(|timing| timing.receive_mono_ns),
server_send_mono_ns: server_timing.and_then(|timing| timing.send_mono_ns),
server_receive_wall_ns: server_timing.and_then(|timing| timing.receive_wall_ns),
server_send_wall_ns: server_timing.and_then(|timing| timing.send_wall_ns),
},
}
}
}
fn system_time_ns(time: SystemTime) -> Option<i128> {
if let Ok(duration) = time.duration_since(UNIX_EPOCH) {
return Some(duration_ns_i128(duration));
}
UNIX_EPOCH
.duration_since(time)
.ok()
.map(|duration| -duration_ns_i128(duration))
}
fn duration_ns_i128(duration: Duration) -> i128 {
i128::try_from(duration.as_nanos()).unwrap_or(i128::MAX)
}
fn signed_duration_ns(duration: SignedDuration) -> i128 {
duration.ns
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn system_time_before_epoch_is_supported() {
let before = UNIX_EPOCH - Duration::from_nanos(7);
assert_eq!(system_time_ns(before), Some(-7));
let after = UNIX_EPOCH + Duration::from_nanos(7);
assert_eq!(system_time_ns(after), Some(7));
let now = SystemTime::now();
assert!(system_time_ns(now).is_some());
}
}
+57
View File
@@ -0,0 +1,57 @@
use std::{collections::VecDeque, time::Duration};
use crate::{core::CoreStats, normalization::StatsEvent, SampleMode, Snapshot, StatsConfig};
#[derive(Debug, Clone, PartialEq)]
pub(crate) struct RollingEvents {
count_limit: Option<usize>,
time_limit: Option<Duration>,
count_events: Option<VecDeque<StatsEvent>>,
time_events: Option<VecDeque<StatsEvent>>,
}
impl RollingEvents {
pub(crate) fn new(config: StatsConfig) -> Self {
Self {
count_limit: config.rolling_count,
time_limit: config.rolling_time,
count_events: config.rolling_count.map(|_| VecDeque::new()),
time_events: config.rolling_time.map(|_| VecDeque::new()),
}
}
pub(crate) fn push(&mut self, event: StatsEvent) {
if let (Some(limit), Some(window)) = (self.count_limit, self.count_events.as_mut()) {
window.push_back(event.clone());
while window.len() > limit {
window.pop_front();
}
}
if let (Some(duration), Some(window)) = (self.time_limit, self.time_events.as_mut()) {
let cutoff = event.at().checked_sub(duration);
window.push_back(event);
if let Some(cutoff) = cutoff {
while window.front().is_some_and(|event| event.at() < cutoff) {
window.pop_front();
}
}
}
}
pub(crate) fn count_snapshot(&self) -> Option<Snapshot> {
self.count_events.as_ref().map(snapshot_window)
}
pub(crate) fn time_snapshot(&self) -> Option<Snapshot> {
self.time_events.as_ref().map(snapshot_window)
}
}
fn snapshot_window(events: &VecDeque<StatsEvent>) -> Snapshot {
let mut core = CoreStats::new(SampleMode::RunningOnly);
for event in events {
core.apply(event.clone());
}
core.snapshot()
}
+157
View File
@@ -0,0 +1,157 @@
#[derive(Debug, Clone, Copy, PartialEq)]
/// Summary statistics for timing values.
///
/// All fields use nanoseconds except `variance_ns2`, which is nanoseconds
/// squared. Values may represent signed timing quantities such as scheduling
/// error or one-way delay.
pub struct TimeStats {
/// Number of samples included in this summary.
pub count: u64,
/// Sum of all samples, in nanoseconds.
pub total_ns: i128,
/// Smallest sample, in nanoseconds.
pub min_ns: Option<i128>,
/// Largest sample, in nanoseconds.
pub max_ns: Option<i128>,
/// Arithmetic mean, in nanoseconds.
pub mean_ns: f64,
/// Median, in nanoseconds, when exact samples were retained.
pub median_ns: Option<f64>,
/// Sample variance, in nanoseconds squared.
pub variance_ns2: f64,
}
impl TimeStats {
/// Returns the sample standard deviation, in nanoseconds.
pub fn stddev_ns(&self) -> f64 {
self.variance_ns2.sqrt()
}
/// Returns whether this summary contains no samples.
pub fn is_empty(&self) -> bool {
self.count == 0
}
}
#[derive(Debug, Clone, PartialEq)]
pub(crate) struct TimeMetric {
running: RunningStats,
samples: Option<Vec<i128>>,
}
impl TimeMetric {
pub(crate) fn new(retain_samples: bool) -> Self {
Self {
running: RunningStats::default(),
samples: retain_samples.then(Vec::new),
}
}
pub(crate) fn push_ns(&mut self, value: i128) {
self.running.push(value);
if let Some(samples) = self.samples.as_mut() {
samples.push(value);
}
}
pub(crate) fn stats(&self) -> TimeStats {
self.running
.stats(self.samples.as_ref().and_then(|samples| median_ns(samples)))
}
}
#[derive(Debug, Clone, PartialEq, Default)]
struct RunningStats {
count: u64,
total_ns: i128,
min_ns: Option<i128>,
max_ns: Option<i128>,
mean_ns: f64,
m2_ns2: f64,
}
impl RunningStats {
fn push(&mut self, value: i128) {
self.count += 1;
self.total_ns = self.total_ns.saturating_add(value);
self.min_ns = Some(self.min_ns.map_or(value, |min| min.min(value)));
self.max_ns = Some(self.max_ns.map_or(value, |max| max.max(value)));
let x = value as f64;
let delta = x - self.mean_ns;
self.mean_ns += delta / self.count as f64;
let delta2 = x - self.mean_ns;
self.m2_ns2 += delta * delta2;
}
fn stats(&self, median_ns: Option<f64>) -> TimeStats {
TimeStats {
count: self.count,
total_ns: self.total_ns,
min_ns: self.min_ns,
max_ns: self.max_ns,
mean_ns: if self.count == 0 { 0.0 } else { self.mean_ns },
median_ns,
variance_ns2: sample_variance(self.count, self.m2_ns2),
}
}
}
fn sample_variance(count: u64, m2: f64) -> f64 {
if count < 2 {
0.0
} else {
m2 / (count - 1) as f64
}
}
fn median_ns(samples: &[i128]) -> Option<f64> {
if samples.is_empty() {
return None;
}
let mut sorted = samples.to_vec();
sorted.sort_unstable();
let mid = sorted.len() / 2;
Some(if sorted.len() % 2 == 1 {
sorted[mid] as f64
} else {
(sorted[mid - 1] as f64 + sorted[mid] as f64) / 2.0
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn running_duration_stats_use_sample_variance() {
let mut metric = TimeMetric::new(false);
metric.push_ns(1);
metric.push_ns(2);
metric.push_ns(3);
let stats = metric.stats();
assert_eq!(stats.count, 3);
assert_eq!(stats.total_ns, 6);
assert_eq!(stats.min_ns, Some(1));
assert_eq!(stats.max_ns, Some(3));
assert_eq!(stats.mean_ns, 2.0);
assert_eq!(stats.variance_ns2, 1.0);
assert_eq!(stats.stddev_ns(), 1.0);
}
#[test]
fn exact_median_handles_odd_and_even_samples() {
assert_eq!(median_ns(&[3, 1, 2]), Some(2.0));
assert_eq!(median_ns(&[4, 1, 2, 3]), Some(2.5));
assert_eq!(median_ns(&[-5, 1, 3]), Some(1.0));
assert_eq!(median_ns(&[-5, 1, 3, 7]), Some(2.0));
}
#[test]
fn single_sample_stddev_is_zero() {
let mut metric = TimeMetric::new(false);
metric.push_ns(42);
let stats = metric.stats();
assert_eq!(stats.variance_ns2, 0.0);
assert_eq!(stats.stddev_ns(), 0.0);
}
}
+114
View File
@@ -0,0 +1,114 @@
use std::time::{Duration, Instant, UNIX_EPOCH};
use irtt_client::{
ClientEvent, ClientTimestamp, OneWayDelaySample, PacketMeta, ReceivedStatsSample, RttSample,
ServerTiming, SignedDuration,
};
pub fn ts(ms: u64) -> ClientTimestamp {
ClientTimestamp {
mono: Instant::now() + Duration::from_millis(ms),
wall: UNIX_EPOCH + Duration::from_millis(ms),
}
}
pub fn rtt(raw_ms: u64, effective_ms: i128) -> RttSample {
RttSample {
raw: Duration::from_millis(raw_ms),
adjusted: u64::try_from(effective_ms).ok().map(Duration::from_millis),
effective: u64::try_from(effective_ms)
.ok()
.map(Duration::from_millis)
.unwrap_or_else(|| Duration::from_millis(raw_ms)),
adjusted_signed: Some(SignedDuration {
ns: effective_ms * 1_000_000,
}),
effective_signed: SignedDuration {
ns: effective_ms * 1_000_000,
},
}
}
pub fn sent(seq: u32, sent_at: ClientTimestamp) -> ClientEvent {
ClientEvent::EchoSent {
seq,
remote: "127.0.0.1:2112".parse().unwrap(),
scheduled_at: sent_at.mono,
sent_at,
bytes: 32,
send_call: Duration::from_micros(10),
timer_error: Duration::from_micros(2),
}
}
pub fn reply(seq: u32, raw_ms: u64, effective_ms: i128) -> ClientEvent {
let sent_at = ts(seq as u64 * 10);
let received_at = ClientTimestamp {
mono: sent_at.mono + Duration::from_millis(raw_ms),
wall: sent_at.wall + Duration::from_millis(raw_ms),
};
ClientEvent::EchoReply {
seq,
remote: "127.0.0.1:2112".parse().unwrap(),
sent_at,
received_at,
rtt: rtt(raw_ms, effective_ms),
server_timing: Some(ServerTiming {
receive_wall_ns: Some(unix_time_ns_after_epoch(sent_at.wall) as i64 + 1_000_000),
receive_mono_ns: Some(seq as i64 * 10_000_000 + 1_000_000),
send_wall_ns: Some(unix_time_ns_after_epoch(sent_at.wall) as i64 + 2_000_000),
send_mono_ns: Some(seq as i64 * 10_000_000 + 2_000_000),
midpoint_wall_ns: None,
midpoint_mono_ns: None,
processing: Some(Duration::from_millis(1)),
}),
one_way: Some(OneWayDelaySample {
client_to_server: Some(Duration::from_millis(1)),
server_to_client: Some(Duration::from_millis(2)),
}),
received_stats: Some(ReceivedStatsSample {
count: Some(seq + 1),
window: Some(0xff),
}),
bytes: 64,
packet_meta: PacketMeta::default(),
}
}
pub fn late_reply(seq: u32, raw_ms: u64, effective_ms: i128) -> ClientEvent {
let ClientEvent::EchoReply {
seq,
remote,
sent_at,
received_at,
rtt,
server_timing,
one_way,
received_stats,
bytes,
packet_meta,
..
} = reply(seq, raw_ms, effective_ms)
else {
unreachable!();
};
ClientEvent::LateReply {
seq,
highest_seen: seq + 1,
remote,
sent_at: Some(sent_at),
received_at,
rtt: Some(rtt),
server_timing,
one_way,
received_stats,
bytes,
packet_meta,
}
}
fn unix_time_ns_after_epoch(time: std::time::SystemTime) -> i128 {
time.duration_since(UNIX_EPOCH)
.map(|duration| duration.as_nanos() as i128)
.unwrap()
}
+272
View File
@@ -0,0 +1,272 @@
mod common;
use std::time::{Duration, Instant};
use common::{late_reply, reply, sent, ts};
use irtt_client::{ClientEvent, PacketMeta};
use irtt_stats::{EventStatsUpdate, IpdvPairUpdate, SampleMode, StatsCollector, StatsConfig};
#[test]
fn running_only_samples_avoid_finite_retention() {
let mut collector = StatsCollector::new(StatsConfig::continuous());
collector.process(&reply(0, 10, 9));
collector.process(&reply(1, 20, 19));
let snapshot = collector.snapshot();
assert_eq!(snapshot.rtt.primary.median_ns, None);
assert_eq!(snapshot.rtt.raw.median_ns, None);
assert_eq!(snapshot.rtt.adjusted.median_ns, None);
assert_eq!(snapshot.ipdv.round_trip.median_ns, None);
assert_eq!(snapshot.one_way_delay.send_delay.median_ns, None);
assert_eq!(snapshot.one_way_delay.receive_delay.median_ns, None);
}
#[test]
fn continuous_mode_tracks_running_samples_without_exact_medians() {
let mut collector = StatsCollector::new(StatsConfig::continuous());
for seq in 0..4104 {
collector.process(&reply(seq, 10, 10));
}
let snapshot = collector.snapshot();
assert_eq!(snapshot.rtt.primary.count, 4104);
assert_eq!(snapshot.ipdv.round_trip.count, 4103);
assert_eq!(snapshot.rtt.primary.median_ns, None);
assert_eq!(snapshot.ipdv.round_trip.median_ns, None);
}
#[test]
fn cumulative_rtt_uses_signed_effective_and_tracks_raw() {
let mut collector = StatsCollector::new(StatsConfig::finite());
collector.process(&reply(0, 1, -2));
collector.process(&reply(1, 10, 8));
let snapshot = collector.snapshot();
assert_eq!(snapshot.rtt.primary.count, 2);
assert_eq!(snapshot.rtt.primary.min_ns, Some(-2_000_000));
assert_eq!(snapshot.rtt.primary.median_ns, Some(3_000_000.0));
assert_eq!(snapshot.rtt.raw.total_ns, 11_000_000);
}
#[test]
fn late_unique_counts_and_duplicates_do_not_update_measurements() {
let mut collector = StatsCollector::new(StatsConfig::finite());
collector.process(&sent(0, ts(0)));
collector.process(&sent(1, ts(10)));
collector.process(&reply(1, 10, 9));
collector.process(&late_reply(0, 20, 19));
collector.process(&ClientEvent::DuplicateReply {
seq: 0,
remote: "127.0.0.1:2112".parse().unwrap(),
received_at: ts(50),
bytes: 64,
});
let snapshot = collector.snapshot();
assert_eq!(snapshot.packets.packets_sent, 2);
assert_eq!(snapshot.packets.packets_received, 3);
assert_eq!(snapshot.packets.unique_replies, 2);
assert_eq!(snapshot.packets.duplicates, 1);
assert_eq!(snapshot.packets.late_packets, 1);
assert_eq!(snapshot.rtt.primary.count, 2);
assert_eq!(snapshot.loss.lost_packets, 0);
assert_eq!(snapshot.loss.duplicate_percent, 100.0 / 3.0);
}
#[test]
fn final_loss_uses_sent_minus_unique_replies_not_echo_loss_events() {
let mut collector = StatsCollector::new(StatsConfig::finite());
collector.process(&sent(0, ts(0)));
collector.process(&sent(1, ts(10)));
collector.process(&ClientEvent::EchoLoss {
seq: 0,
sent_at: ts(0),
timeout_at: Instant::now(),
});
collector.process(&late_reply(0, 10, 9));
let snapshot = collector.snapshot();
assert_eq!(snapshot.events.loss_events, 1);
assert_eq!(snapshot.packets.unique_replies, 1);
assert_eq!(snapshot.loss.lost_packets, 1);
assert_eq!(snapshot.loss.packet_loss_percent, 50.0);
}
fn assert_no_ipdv_pairs(update: &EventStatsUpdate) {
assert!(update.ipdv_pairs.is_empty(), "{update:?}");
}
fn assert_one_ipdv_pair(
update: &EventStatsUpdate,
previous_seq: u32,
current_seq: u32,
rtt_ipdv: Duration,
) -> &IpdvPairUpdate {
assert_eq!(update.ipdv_pairs.len(), 1, "{update:?}");
let pair = &update.ipdv_pairs[0];
assert_eq!(pair.previous_seq, previous_seq);
assert_eq!(pair.current_seq, current_seq);
assert_eq!(pair.rtt_ipdv, rtt_ipdv);
pair
}
#[test]
fn ipdv_is_sequence_adjacent_and_gap_preserving() {
let mut collector = StatsCollector::new(StatsConfig::finite());
let first = collector.process(&reply(0, 10, 10));
let gap = collector.process(&reply(2, 15, 15));
let adjacent = collector.process(&reply(3, 12, 12));
let snapshot = collector.snapshot();
assert!(first.contributed_sample);
assert_no_ipdv_pairs(&first);
assert!(gap.contributed_sample);
assert_no_ipdv_pairs(&gap);
assert!(adjacent.contributed_sample);
assert_one_ipdv_pair(&adjacent, 2, 3, Duration::from_millis(3));
assert_eq!(snapshot.ipdv.round_trip.count, 1);
assert_eq!(snapshot.ipdv.round_trip.total_ns, 3_000_000);
}
#[test]
fn late_reply_can_complete_ipdv_pair() {
let mut collector = StatsCollector::new(StatsConfig::finite());
collector.process(&reply(1, 20, 20));
let update = collector.process(&late_reply(0, 10, 10));
let snapshot = collector.snapshot();
assert!(update.contributed_sample);
assert_one_ipdv_pair(&update, 0, 1, Duration::from_millis(10));
assert_eq!(snapshot.ipdv.round_trip.count, 1);
assert_eq!(snapshot.ipdv.round_trip.total_ns, 10_000_000);
}
#[test]
fn update_exposes_directional_ipdv_when_available() {
let mut collector = StatsCollector::new(StatsConfig::finite());
collector.process(&reply(0, 10, 10));
let update = collector.process(&reply(1, 13, 13));
assert!(update.contributed_sample);
let pair = assert_one_ipdv_pair(&update, 0, 1, Duration::from_millis(3));
assert!(pair.send_ipdv.is_some());
assert!(pair.receive_ipdv.is_some());
}
#[test]
fn gap_fill_update_exposes_both_completed_ipdv_pairs() {
let mut collector = StatsCollector::new(StatsConfig::finite());
let first = collector.process(&reply(0, 10, 10));
let gap = collector.process(&reply(2, 20, 20));
let fill = collector.process(&reply(1, 13, 13));
assert!(first.contributed_sample);
assert!(first.ipdv_pairs.is_empty());
assert!(gap.contributed_sample);
assert!(gap.ipdv_pairs.is_empty());
assert!(fill.contributed_sample);
assert_eq!(fill.ipdv_pairs.len(), 2);
assert_eq!(fill.ipdv_pairs[0].previous_seq, 0);
assert_eq!(fill.ipdv_pairs[0].current_seq, 1);
assert_eq!(fill.ipdv_pairs[0].rtt_ipdv, Duration::from_millis(3));
assert_eq!(fill.ipdv_pairs[1].previous_seq, 1);
assert_eq!(fill.ipdv_pairs[1].current_seq, 2);
assert_eq!(fill.ipdv_pairs[1].rtt_ipdv, Duration::from_millis(7));
let snapshot = collector.snapshot();
assert_eq!(snapshot.ipdv.round_trip.count, 2);
assert_eq!(snapshot.ipdv.round_trip.total_ns, 10_000_000);
}
#[test]
fn server_processing_and_one_way_require_available_samples() {
let mut collector = StatsCollector::new(StatsConfig::finite());
collector.process(&reply(0, 10, 9));
collector.process(&ClientEvent::LateReply {
seq: 9,
highest_seen: 10,
remote: "127.0.0.1:2112".parse().unwrap(),
sent_at: None,
received_at: ts(100),
rtt: None,
server_timing: None,
one_way: None,
received_stats: None,
bytes: 64,
packet_meta: PacketMeta::default(),
});
let snapshot = collector.snapshot();
assert_eq!(snapshot.server_processing.processing.count, 1);
assert_eq!(snapshot.one_way_delay.send_delay.count, 1);
assert_eq!(snapshot.events.untracked_late_replies, 1);
}
#[test]
fn rolling_count_eviction_recomputes_from_bounded_events() {
let mut collector = StatsCollector::new(StatsConfig {
samples: SampleMode::RunningOnly,
rolling_count: Some(2),
rolling_time: None,
});
collector.process(&sent(0, ts(0)));
collector.process(&reply(0, 10, 10));
collector.process(&reply(1, 20, 20));
let rolling = collector.rolling_count().unwrap();
assert_eq!(rolling.packets.packets_sent, 0);
assert_eq!(rolling.packets.unique_replies, 2);
assert_eq!(rolling.rtt.primary.count, 2);
}
#[test]
fn rolling_time_eviction_uses_event_timestamps() {
let mut collector = StatsCollector::new(StatsConfig {
samples: SampleMode::RunningOnly,
rolling_count: None,
rolling_time: Some(Duration::from_millis(15)),
});
collector.process(&sent(0, ts(0)));
collector.process(&sent(1, ts(10)));
collector.process(&sent(2, ts(30)));
let rolling = collector.rolling_time().unwrap();
assert_eq!(rolling.packets.packets_sent, 1);
}
#[test]
fn empty_and_all_lost_edges_are_defined() {
let empty = StatsCollector::new(StatsConfig::finite()).snapshot();
assert_eq!(empty.loss.packet_loss_percent, 0.0);
let mut collector = StatsCollector::new(StatsConfig::finite());
collector.process(&sent(0, ts(0)));
let all_lost = collector.snapshot();
assert_eq!(all_lost.loss.lost_packets, 1);
assert_eq!(all_lost.loss.packet_loss_percent, 100.0);
}
#[test]
fn directional_loss_uses_server_received_count_when_available() {
let mut collector = StatsCollector::new(StatsConfig::finite());
collector.process(&sent(0, ts(0)));
collector.process(&sent(1, ts(10)));
collector.process(&reply(0, 10, 10));
let loss = collector.snapshot().loss;
assert_eq!(loss.lost_packets, 1);
assert_eq!(loss.upstream_loss_packets, Some(1));
assert_eq!(loss.downstream_loss_packets, Some(0));
assert_eq!(loss.packet_loss_percent, 50.0);
assert_eq!(loss.upstream_loss_percent, 50.0);
}
+110
View File
@@ -0,0 +1,110 @@
# cargo-deny policy for irtt-rs.
#
# Goals:
# - Check the full dependency graph with all features enabled.
# - Fail on known vulnerable/yanked crates.
# - Keep accepted licenses narrow and explicit.
# - Warn on duplicate crate versions for now.
# - Deny unexpected registries or git dependencies.
# - If git dependencies are ever allowed, require exact commit pins.
[graph]
all-features = true
no-default-features = false
targets = []
[output]
feature-depth = 1
[advisories]
# Yanked crates are usually not an immediate vuln, but they are almost never
# something we want to keep resolving in a public crate.
yanked = "deny"
# Unmaintained advisories are useful, but failing on every transitive
# unmaintained crate can become dependency-tree hostage negotiation.
# Direct/workspace dependencies should still be treated seriously.
unmaintained = "workspace"
# Keep this explicit. cargo-deny's current default is also workspace, but this
# makes the policy obvious.
unsound = "workspace"
ignore = [
# Example:
# { id = "RUSTSEC-0000-0000", reason = "Not reachable: explain why." },
# { crate = "some-yanked-crate@1.2.3", reason = "Temporary until upstream releases a replacement." },
]
[licenses]
confidence-threshold = 0.8
allow = [
"MIT",
"Apache-2.0",
"Unicode-3.0",
"BSD-3-Clause",
]
exceptions = [
# Example:
# { crate = "some-crate", allow = ["BSD-2-Clause"] },
]
[licenses.private]
ignore = false
registries = []
[bans]
# Keep as warn initially. Making this deny too early is how you end up doing
# dependency bonsai instead of actual work.
multiple-versions = "warn"
# Consider changing this to "deny" later once you've confirmed nothing relies
# on wildcard requirements.
wildcards = "allow"
highlight = "all"
workspace-default-features = "allow"
external-default-features = "allow"
allow = []
deny = [
# Example:
# { crate = "openssl", reason = "Prefer rustls where TLS is needed." },
]
skip = [
# Example:
# { crate = "windows-sys@0.52.0", reason = "Transitive duplicate; waiting on upstream." },
]
skip-tree = [
# Example:
# { crate = "some-large-transitive-root@1.2.3", reason = "Temporary duplicate tree exception." },
]
[sources]
# Unknown dependency sources should fail. If we need a git patch later,
# we add it explicitly and remove it again when upstream releases.
unknown-registry = "deny"
unknown-git = "deny"
# If git dependencies are ever allowed, require exact commit pins.
# cargo-deny's docs list "rev" as the most specific git source requirement.
required-git-spec = "rev"
# crates.io registry. cargo-deny's docs say this is also the default when
# allow-registry is omitted, but keeping it explicit is fine.
allow-registry = [
"https://github.com/rust-lang/crates.io-index",
]
allow-git = []
[sources.allow-org]
github = []
gitlab = []
bitbucket = []
+13
View File
@@ -0,0 +1,13 @@
[workspace]
members = ["cargo:."]
# Config for 'dist'
[dist]
# The preferred dist version to use in CI (Cargo.toml SemVer syntax)
cargo-dist-version = "0.31.0"
# CI backends to support
ci = "github"
# The installers to generate for each app
installers = []
# Target platforms to build apps for (Rust target-triple syntax)
targets = ["aarch64-apple-darwin", "aarch64-unknown-linux-gnu", "x86_64-apple-darwin", "x86_64-unknown-linux-gnu", "x86_64-unknown-linux-musl", "x86_64-pc-windows-msvc"]
+70
View File
@@ -0,0 +1,70 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": ["config:recommended", "helpers:pinGitHubActionDigests"],
"timezone": "Asia/Jakarta",
"schedule": ["before 10am on monday"],
"dependencyDashboard": true,
"dependencyDashboardTitle": "Renovate Dashboard",
"semanticCommits": "enabled",
"semanticCommitType": "chore",
"semanticCommitScope": "deps",
"prConcurrentLimit": 4,
"prHourlyLimit": 1,
"rebaseWhen": "conflicted",
"rangeStrategy": "bump",
"labels": ["dependencies"],
"lockFileMaintenance": {
"enabled": true,
"schedule": ["before 10am on monday"],
"commitMessageAction": "maintain",
"commitMessageTopic": "Cargo lockfile"
},
"packageRules": [
{
"description": "Never automerge Cargo updates by default",
"matchManagers": ["cargo"],
"automerge": false
},
{
"description": "Group RustCrypto updates together",
"matchManagers": ["cargo"],
"matchPackageNames": ["hmac", "md-5"],
"groupName": "RustCrypto dependencies",
"commitMessageTopic": "RustCrypto dependencies"
},
{
"description": "Keep socket/platform dependencies together but reviewed",
"matchManagers": ["cargo"],
"matchPackageNames": ["socket2", "libc"],
"groupName": "socket dependencies",
"commitMessageTopic": "socket dependencies"
},
{
"description": "Group GitHub Actions updates",
"matchManagers": ["github-actions"],
"groupName": "GitHub Actions",
"commitMessageTopic": "GitHub Actions"
},
{
"description": "Automerge patch and minor GitHub Actions updates",
"matchManagers": ["github-actions"],
"matchUpdateTypes": ["patch", "minor"],
"automerge": true,
"platformAutomerge": true
},
{
"description": "Never automerge major updates",
"matchUpdateTypes": ["major"],
"automerge": false
}
]
}