Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 63 additions & 0 deletions .github/workflows/windows-probe.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# Probe only, for the fork: never part of an upstream PR.
name: Windows probe

on:
push:
branches:
- 'probe/**'
workflow_dispatch:

jobs:
probe:
name: session loop ${{ matrix.mode }} py${{ matrix.python-version }}
runs-on: windows-latest
timeout-minutes: 25
strategy:
fail-fast: false
matrix:
python-version: ["3.11", "3.12", "3.13", "3.14", "3.15"]
mode: [xdist]
include:
- python-version: "3.14"
mode: serial
- python-version: "3.15"
mode: serial
steps:
- name: Checkout Source Files
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

- name: Setup Environment
uses: ./.github/actions/setup
with:
python-version: ${{ matrix.python-version }}

- name: Show TCP port settings
shell: bash
run: |
netsh int ipv4 show dynamicport tcp
nproc || true

- name: Run tests and sample TIME_WAIT
shell: bash
run: |
set +e
(while true; do
echo "$(date +%T) TIME_WAIT=$(netstat -an -p tcp | grep -c TIME_WAIT)"
sleep 5
done) > timewait.log 2>&1 &
sampler=$!
start=$(date +%s)
if [ "${{ matrix.mode }}" = serial ]; then
uv run pytest -n0 -p no:sugar -q --cov kasa \
--timeout 90 --timeout-method thread
else
uv run pytest -n auto -p no:sugar -q --cov kasa \
--timeout 90 --timeout-method thread
fi
rc=$?
echo "pytest exit=$rc after $(( $(date +%s) - start ))s"
kill $sampler
echo "--- TIME_WAIT samples (every 5 s)"
cat timewait.log
echo "--- peak TIME_WAIT: $(sed 's/.*TIME_WAIT=//' timewait.log | sort -n | tail -1)"
exit $rc
5 changes: 4 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,10 @@ markers = [
"requires_dummy: test requires dummy data to pass, skipped on real devices",
]
asyncio_mode = "auto"
asyncio_default_fixture_loop_scope = "function"
# One event loop for the run, not one per test: on Windows each loop opens a
# loopback TCP connection, and thousands of them use up the dynamic ports.
asyncio_default_fixture_loop_scope = "session"
asyncio_default_test_loop_scope = "session"
#timeout = 10
# dist=loadgroup enables grouping of tests into single worker.
# required as caplog doesn't play nicely with multiple workers.
Expand Down
31 changes: 0 additions & 31 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@
import asyncio
import functools
import os
import socket
import struct
import sys
import warnings
from pathlib import Path
Expand Down Expand Up @@ -166,35 +164,6 @@ def pytest_collection_modifyitems(
item.add_marker(pytest.mark.enable_socket)


@pytest.fixture(autouse=True, scope="session")
def _socketpair_without_time_wait():
"""Stop event loop self-pipes from using up loopback ports on Windows.

Windows has no native socketpair, so the self-pipe of every event loop is a
loopback TCP connection, and closing it leaves a port in TIME_WAIT for about
two minutes. Tests get a new event loop each, so a fast run uses up the
dynamic port range, and the next loop then fails to start or hangs.
Closing the pair with SO_LINGER set to zero releases the port immediately.
"""
if sys.platform != "win32":
yield
return

orig_socketpair = socket.socketpair

def _socketpair(*args, **kwargs):
pair = orig_socketpair(*args, **kwargs)
for sock in pair:
# struct linger on Windows is two unsigned shorts: l_onoff, l_linger
sock.setsockopt(
socket.SOL_SOCKET, socket.SO_LINGER, struct.pack("HH", 1, 0)
)
return pair

with patch("socket.socketpair", _socketpair):
yield


@pytest.fixture(autouse=True, scope="session")
def asyncio_sleep_fixture(request: pytest.FixtureRequest): # noqa: PT004
"""Patch sleep to prevent tests actually waiting."""
Expand Down
Loading