From 88a98f315eb24d209cbc81d89ff5e8eaf1c1c8d9 Mon Sep 17 00:00:00 2001 From: "riseproject-dev[bot]" <330740410+riseproject-dev[bot]@users.noreply.github.com> Date: Thu, 8 Oct 2026 08:34:05 +0000 Subject: [PATCH 1/3] oai-statsig-python-core: Add versions 0.32.0, 0.33.0, 0.34.0, 0.35.0 Signed-off-by: riseproject-dev[bot] <330740410+riseproject-dev[bot]@users.noreply.github.com> --- docs/packages/oai-statsig-python-core.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/packages/oai-statsig-python-core.yaml b/docs/packages/oai-statsig-python-core.yaml index a64f44d791d..81003d8b845 100644 --- a/docs/packages/oai-statsig-python-core.yaml +++ b/docs/packages/oai-statsig-python-core.yaml @@ -23,3 +23,7 @@ versions: - filename: oai_statsig_python_core-0.31.0-cp310-abi3-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl sha256: 7b0565b6c5439a16e4a100423fe7da11d1cdebda07ec0d3960fcd63a745ba7ef requires-python: '>=3.10' +- version: 0.32.0 +- version: 0.33.0 +- version: 0.34.0 +- version: 0.35.0 From 0655f7350ade93bf716c854ceaeae7fcf3509c56 Mon Sep 17 00:00:00 2001 From: Ludovic Henry Date: Fri, 9 Oct 2026 16:31:05 +0000 Subject: [PATCH 2/3] oai-statsig-python-core: carry forward riscv64 patches to 0.32.0, 0.33.0, 0.34.0, 0.35.0 Nightly version bump omitted the patches/oai-statsig-python-core/ directory for 0.32.0, 0.33.0, 0.34.0, 0.35.0, so the build's unconditional `git apply .../*.patch` step failed with "No such file or directory" before any build/test ran. Copied forward the same patch set from 0.31.0 (unchanged since then). --- ...ackground-specs-syncs-instead-of-ass.patch | 156 ++++++++++++++++++ ...runner.py-a-timeout-a-slow-machine-c.patch | 32 ++++ ...ackground-specs-syncs-instead-of-ass.patch | 156 ++++++++++++++++++ ...runner.py-a-timeout-a-slow-machine-c.patch | 32 ++++ ...ackground-specs-syncs-instead-of-ass.patch | 156 ++++++++++++++++++ ...runner.py-a-timeout-a-slow-machine-c.patch | 32 ++++ ...ackground-specs-syncs-instead-of-ass.patch | 156 ++++++++++++++++++ ...runner.py-a-timeout-a-slow-machine-c.patch | 32 ++++ 8 files changed, 752 insertions(+) create mode 100644 patches/oai-statsig-python-core/0.32.0/0001-tests-wait-for-background-specs-syncs-instead-of-ass.patch create mode 100644 patches/oai-statsig-python-core/0.32.0/0002-tests-give-fork_runner.py-a-timeout-a-slow-machine-c.patch create mode 100644 patches/oai-statsig-python-core/0.33.0/0001-tests-wait-for-background-specs-syncs-instead-of-ass.patch create mode 100644 patches/oai-statsig-python-core/0.33.0/0002-tests-give-fork_runner.py-a-timeout-a-slow-machine-c.patch create mode 100644 patches/oai-statsig-python-core/0.34.0/0001-tests-wait-for-background-specs-syncs-instead-of-ass.patch create mode 100644 patches/oai-statsig-python-core/0.34.0/0002-tests-give-fork_runner.py-a-timeout-a-slow-machine-c.patch create mode 100644 patches/oai-statsig-python-core/0.35.0/0001-tests-wait-for-background-specs-syncs-instead-of-ass.patch create mode 100644 patches/oai-statsig-python-core/0.35.0/0002-tests-give-fork_runner.py-a-timeout-a-slow-machine-c.patch diff --git a/patches/oai-statsig-python-core/0.32.0/0001-tests-wait-for-background-specs-syncs-instead-of-ass.patch b/patches/oai-statsig-python-core/0.32.0/0001-tests-wait-for-background-specs-syncs-instead-of-ass.patch new file mode 100644 index 00000000000..832832f3ec8 --- /dev/null +++ b/patches/oai-statsig-python-core/0.32.0/0001-tests-wait-for-background-specs-syncs-instead-of-ass.patch @@ -0,0 +1,156 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Ludovic Henry +Date: Sun, 20 Sep 2026 22:48:16 +0000 +Subject: [PATCH] tests: wait for background specs syncs instead of assuming a + 1ms interval + +`StatsigOptions` rejects a `specs_sync_interval_ms` below `MIN_SYNC_INTERVAL` +(1000) and drops it back to the default, logging "Invalid +'specs_sync_interval_ms', value must be greater than 1000, received Some(1)". +Both fixtures in this file still ask for 1, so the six polling tests never get +the near-instant background sync they were written against and fail on every +platform -- reproduced here against the published +manylinux_2_17_x86_64 wheel, where 6 of the 7 tests in this file fail. + +Use the smallest accepted interval -- the same 1000 upstream already switched +`test_observability_client.py` to -- and wait for the condition each test is +really about rather than for a fixed duration. The waits return as soon as the +sync lands, so fast hardware pays nothing. + +Upstream-Status: To upstream [not yet submitted; the release is cut from a non-public tree, so this needs a maintainer discussion rather than a drive-by PR] +--- + statsig-pyo3/tests/test_data_store.py | 46 +++++++++++++++++---------- + 1 file changed, 30 insertions(+), 16 deletions(-) + +diff --git a/statsig-pyo3/tests/test_data_store.py b/statsig-pyo3/tests/test_data_store.py +index 9df3ac7..708c81b 100644 +--- a/statsig-pyo3/tests/test_data_store.py ++++ b/statsig-pyo3/tests/test_data_store.py +@@ -17,6 +17,12 @@ from utils import get_test_data_resource, get_test_data_resource_bytes + + known_lcut = 1767981029384 + ++# StatsigOptions rejects a specs_sync_interval_ms below this and falls back to the ++# default, so the polling tests below have to wait for a real sync to land. ++MIN_SYNC_INTERVAL_MS = 1000 ++SYNC_WAIT_ATTEMPTS = 200 ++SYNC_WAIT_INTERVAL_S = 0.05 ++ + dcs_content = get_test_data_resource("eval_proj_dcs.json") + json_data = json.loads(dcs_content) + eval_proj_protobuf = get_test_data_resource_bytes("eval_proj_dcs.pb.br") +@@ -169,7 +175,7 @@ def statsig_setup(httpserver: HTTPServer): + specs_url=httpserver.url_for("/v2/download_config_specs"), + log_event_url=httpserver.url_for("/v1/log_event"), + data_store=data_store, +- specs_sync_interval_ms=1, ++ specs_sync_interval_ms=MIN_SYNC_INTERVAL_MS, + ) + + statsig = Statsig("secret-key", options) +@@ -193,7 +199,7 @@ def statsig_bytes_setup(httpserver: HTTPServer): + specs_url=httpserver.url_for("/v2/download_config_specs"), + log_event_url=httpserver.url_for("/v1/log_event"), + data_store=data_store, +- specs_sync_interval_ms=1, ++ specs_sync_interval_ms=MIN_SYNC_INTERVAL_MS, + ) + + statsig = Statsig("secret-key", options) +@@ -223,10 +229,10 @@ def test_data_store_usage_get(statsig_setup): + assert gate.value == True + assert gate.details.lcut == known_lcut + +- for _ in range(100): ++ for _ in range(SYNC_WAIT_ATTEMPTS): + if data_store.get_called_count >= 2: + break +- sleep(0.05) ++ sleep(SYNC_WAIT_INTERVAL_S) + + assert data_store.get_called_count > 1 + +@@ -240,9 +246,13 @@ def test_data_store_usage_set(statsig_setup): + + assert data_store.init_called + assert gate.details.reason == "Adapter(DataStore):Recognized" +- sleep(1) + +- gate_after = statsig.get_feature_gate(user, "test_public") ++ for _ in range(SYNC_WAIT_ATTEMPTS): ++ gate_after = statsig.get_feature_gate(user, "test_public") ++ if gate_after.details.lcut == known_lcut + 10: ++ break ++ sleep(SYNC_WAIT_INTERVAL_S) ++ + statsig.flush_events().wait() + + assert gate_after.value == True +@@ -265,10 +275,10 @@ def test_data_store_usage_get_bytes(statsig_bytes_setup): + assert gate.value == True + assert gate.details.lcut == known_lcut + +- for _ in range(5): ++ for _ in range(SYNC_WAIT_ATTEMPTS): + if data_store.set_bytes_called_count > 0: + break +- sleep(0.05) ++ sleep(SYNC_WAIT_INTERVAL_S) + + assert data_store.get_bytes_called_count >= 1 + assert data_store.get_called_count == 0 +@@ -283,10 +293,10 @@ def test_data_store_usage_get_bytes_request_has_since_time_after_initial_poll(st + gate = statsig.get_feature_gate(user, "test_public") + assert gate.details.reason == "Adapter(DataStore):Recognized" + +- for _ in range(100): ++ for _ in range(SYNC_WAIT_ATTEMPTS): + if data_store.get_bytes_called_count >= 2: + break +- sleep(0.05) ++ sleep(SYNC_WAIT_INTERVAL_S) + + assert data_store.get_bytes_called_count >= 2 + +@@ -308,10 +318,10 @@ def test_data_store_usage_get_bytes_request_checksum_match_returns_no_update(sta + gate = statsig.get_feature_gate(user, "test_public") + assert gate.details.reason == "Adapter(DataStore):Recognized" + +- for _ in range(100): ++ for _ in range(SYNC_WAIT_ATTEMPTS): + if data_store.get_bytes_called_count >= 2: + break +- sleep(0.05) ++ sleep(SYNC_WAIT_INTERVAL_S) + + assert data_store.get_bytes_called_count >= 2 + assert data_store.returned_no_update +@@ -325,10 +335,10 @@ def test_data_store_usage_get_bytes_request_checksum_match_returns_no_update(sta + assert request_with_checksum.since_time == data_store.stored_time + + # no second write should occur when server responds with {"has_updates": false} +- for _ in range(100): ++ for _ in range(SYNC_WAIT_ATTEMPTS): + if data_store.get_bytes_called_count > 2: + break +- sleep(0.05) ++ sleep(SYNC_WAIT_INTERVAL_S) + + + +@@ -341,9 +351,13 @@ def test_data_store_usage_set_bytes(statsig_bytes_setup): + + assert data_store.init_called + assert gate.details.reason == "Adapter(DataStore):Recognized" +- sleep(1) + +- gate_after = statsig.get_feature_gate(user, "test_public") ++ for _ in range(SYNC_WAIT_ATTEMPTS): ++ gate_after = statsig.get_feature_gate(user, "test_public") ++ if gate_after.details.lcut == known_lcut + 10: ++ break ++ sleep(SYNC_WAIT_INTERVAL_S) ++ + statsig.flush_events().wait() + + assert gate_after.value == True diff --git a/patches/oai-statsig-python-core/0.32.0/0002-tests-give-fork_runner.py-a-timeout-a-slow-machine-c.patch b/patches/oai-statsig-python-core/0.32.0/0002-tests-give-fork_runner.py-a-timeout-a-slow-machine-c.patch new file mode 100644 index 00000000000..82afe1f1a09 --- /dev/null +++ b/patches/oai-statsig-python-core/0.32.0/0002-tests-give-fork_runner.py-a-timeout-a-slow-machine-c.patch @@ -0,0 +1,32 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Ludovic Henry +Date: Sun, 20 Sep 2026 22:48:16 +0000 +Subject: [PATCH] tests: give fork_runner.py a timeout a slow machine can meet + +`fork_runner.py` performs 50 full SDK initialisations -- ten iterations, each +followed by four nested forks that initialise, evaluate and shut down -- against +a local mock server. Ten seconds is enough on upstream's x86_64 CI and not on a +riscv64 runner, where the subprocess is SIGTERMed part way through and the test +fails with `assert -15 == 0`. + +`communicate()` returns as soon as the subprocess exits, so a larger budget +costs nothing where the old one was already sufficient. + +Upstream-Status: To upstream [not yet submitted; the release is cut from a non-public tree, so this needs a maintainer discussion rather than a drive-by PR] +--- + statsig-pyo3/tests/test_forking.py | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/statsig-pyo3/tests/test_forking.py b/statsig-pyo3/tests/test_forking.py +index a5f27c4..8d2bd7d 100644 +--- a/statsig-pyo3/tests/test_forking.py ++++ b/statsig-pyo3/tests/test_forking.py +@@ -45,7 +45,7 @@ def test_forking(httpserver: HTTPServer): + env={**os.environ, "RUST_BACKTRACE": "full"}, + ) + try: +- proc.communicate(timeout=10) ++ proc.communicate(timeout=180) + except subprocess.TimeoutExpired: + proc.terminate() + proc.wait() diff --git a/patches/oai-statsig-python-core/0.33.0/0001-tests-wait-for-background-specs-syncs-instead-of-ass.patch b/patches/oai-statsig-python-core/0.33.0/0001-tests-wait-for-background-specs-syncs-instead-of-ass.patch new file mode 100644 index 00000000000..832832f3ec8 --- /dev/null +++ b/patches/oai-statsig-python-core/0.33.0/0001-tests-wait-for-background-specs-syncs-instead-of-ass.patch @@ -0,0 +1,156 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Ludovic Henry +Date: Sun, 20 Sep 2026 22:48:16 +0000 +Subject: [PATCH] tests: wait for background specs syncs instead of assuming a + 1ms interval + +`StatsigOptions` rejects a `specs_sync_interval_ms` below `MIN_SYNC_INTERVAL` +(1000) and drops it back to the default, logging "Invalid +'specs_sync_interval_ms', value must be greater than 1000, received Some(1)". +Both fixtures in this file still ask for 1, so the six polling tests never get +the near-instant background sync they were written against and fail on every +platform -- reproduced here against the published +manylinux_2_17_x86_64 wheel, where 6 of the 7 tests in this file fail. + +Use the smallest accepted interval -- the same 1000 upstream already switched +`test_observability_client.py` to -- and wait for the condition each test is +really about rather than for a fixed duration. The waits return as soon as the +sync lands, so fast hardware pays nothing. + +Upstream-Status: To upstream [not yet submitted; the release is cut from a non-public tree, so this needs a maintainer discussion rather than a drive-by PR] +--- + statsig-pyo3/tests/test_data_store.py | 46 +++++++++++++++++---------- + 1 file changed, 30 insertions(+), 16 deletions(-) + +diff --git a/statsig-pyo3/tests/test_data_store.py b/statsig-pyo3/tests/test_data_store.py +index 9df3ac7..708c81b 100644 +--- a/statsig-pyo3/tests/test_data_store.py ++++ b/statsig-pyo3/tests/test_data_store.py +@@ -17,6 +17,12 @@ from utils import get_test_data_resource, get_test_data_resource_bytes + + known_lcut = 1767981029384 + ++# StatsigOptions rejects a specs_sync_interval_ms below this and falls back to the ++# default, so the polling tests below have to wait for a real sync to land. ++MIN_SYNC_INTERVAL_MS = 1000 ++SYNC_WAIT_ATTEMPTS = 200 ++SYNC_WAIT_INTERVAL_S = 0.05 ++ + dcs_content = get_test_data_resource("eval_proj_dcs.json") + json_data = json.loads(dcs_content) + eval_proj_protobuf = get_test_data_resource_bytes("eval_proj_dcs.pb.br") +@@ -169,7 +175,7 @@ def statsig_setup(httpserver: HTTPServer): + specs_url=httpserver.url_for("/v2/download_config_specs"), + log_event_url=httpserver.url_for("/v1/log_event"), + data_store=data_store, +- specs_sync_interval_ms=1, ++ specs_sync_interval_ms=MIN_SYNC_INTERVAL_MS, + ) + + statsig = Statsig("secret-key", options) +@@ -193,7 +199,7 @@ def statsig_bytes_setup(httpserver: HTTPServer): + specs_url=httpserver.url_for("/v2/download_config_specs"), + log_event_url=httpserver.url_for("/v1/log_event"), + data_store=data_store, +- specs_sync_interval_ms=1, ++ specs_sync_interval_ms=MIN_SYNC_INTERVAL_MS, + ) + + statsig = Statsig("secret-key", options) +@@ -223,10 +229,10 @@ def test_data_store_usage_get(statsig_setup): + assert gate.value == True + assert gate.details.lcut == known_lcut + +- for _ in range(100): ++ for _ in range(SYNC_WAIT_ATTEMPTS): + if data_store.get_called_count >= 2: + break +- sleep(0.05) ++ sleep(SYNC_WAIT_INTERVAL_S) + + assert data_store.get_called_count > 1 + +@@ -240,9 +246,13 @@ def test_data_store_usage_set(statsig_setup): + + assert data_store.init_called + assert gate.details.reason == "Adapter(DataStore):Recognized" +- sleep(1) + +- gate_after = statsig.get_feature_gate(user, "test_public") ++ for _ in range(SYNC_WAIT_ATTEMPTS): ++ gate_after = statsig.get_feature_gate(user, "test_public") ++ if gate_after.details.lcut == known_lcut + 10: ++ break ++ sleep(SYNC_WAIT_INTERVAL_S) ++ + statsig.flush_events().wait() + + assert gate_after.value == True +@@ -265,10 +275,10 @@ def test_data_store_usage_get_bytes(statsig_bytes_setup): + assert gate.value == True + assert gate.details.lcut == known_lcut + +- for _ in range(5): ++ for _ in range(SYNC_WAIT_ATTEMPTS): + if data_store.set_bytes_called_count > 0: + break +- sleep(0.05) ++ sleep(SYNC_WAIT_INTERVAL_S) + + assert data_store.get_bytes_called_count >= 1 + assert data_store.get_called_count == 0 +@@ -283,10 +293,10 @@ def test_data_store_usage_get_bytes_request_has_since_time_after_initial_poll(st + gate = statsig.get_feature_gate(user, "test_public") + assert gate.details.reason == "Adapter(DataStore):Recognized" + +- for _ in range(100): ++ for _ in range(SYNC_WAIT_ATTEMPTS): + if data_store.get_bytes_called_count >= 2: + break +- sleep(0.05) ++ sleep(SYNC_WAIT_INTERVAL_S) + + assert data_store.get_bytes_called_count >= 2 + +@@ -308,10 +318,10 @@ def test_data_store_usage_get_bytes_request_checksum_match_returns_no_update(sta + gate = statsig.get_feature_gate(user, "test_public") + assert gate.details.reason == "Adapter(DataStore):Recognized" + +- for _ in range(100): ++ for _ in range(SYNC_WAIT_ATTEMPTS): + if data_store.get_bytes_called_count >= 2: + break +- sleep(0.05) ++ sleep(SYNC_WAIT_INTERVAL_S) + + assert data_store.get_bytes_called_count >= 2 + assert data_store.returned_no_update +@@ -325,10 +335,10 @@ def test_data_store_usage_get_bytes_request_checksum_match_returns_no_update(sta + assert request_with_checksum.since_time == data_store.stored_time + + # no second write should occur when server responds with {"has_updates": false} +- for _ in range(100): ++ for _ in range(SYNC_WAIT_ATTEMPTS): + if data_store.get_bytes_called_count > 2: + break +- sleep(0.05) ++ sleep(SYNC_WAIT_INTERVAL_S) + + + +@@ -341,9 +351,13 @@ def test_data_store_usage_set_bytes(statsig_bytes_setup): + + assert data_store.init_called + assert gate.details.reason == "Adapter(DataStore):Recognized" +- sleep(1) + +- gate_after = statsig.get_feature_gate(user, "test_public") ++ for _ in range(SYNC_WAIT_ATTEMPTS): ++ gate_after = statsig.get_feature_gate(user, "test_public") ++ if gate_after.details.lcut == known_lcut + 10: ++ break ++ sleep(SYNC_WAIT_INTERVAL_S) ++ + statsig.flush_events().wait() + + assert gate_after.value == True diff --git a/patches/oai-statsig-python-core/0.33.0/0002-tests-give-fork_runner.py-a-timeout-a-slow-machine-c.patch b/patches/oai-statsig-python-core/0.33.0/0002-tests-give-fork_runner.py-a-timeout-a-slow-machine-c.patch new file mode 100644 index 00000000000..82afe1f1a09 --- /dev/null +++ b/patches/oai-statsig-python-core/0.33.0/0002-tests-give-fork_runner.py-a-timeout-a-slow-machine-c.patch @@ -0,0 +1,32 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Ludovic Henry +Date: Sun, 20 Sep 2026 22:48:16 +0000 +Subject: [PATCH] tests: give fork_runner.py a timeout a slow machine can meet + +`fork_runner.py` performs 50 full SDK initialisations -- ten iterations, each +followed by four nested forks that initialise, evaluate and shut down -- against +a local mock server. Ten seconds is enough on upstream's x86_64 CI and not on a +riscv64 runner, where the subprocess is SIGTERMed part way through and the test +fails with `assert -15 == 0`. + +`communicate()` returns as soon as the subprocess exits, so a larger budget +costs nothing where the old one was already sufficient. + +Upstream-Status: To upstream [not yet submitted; the release is cut from a non-public tree, so this needs a maintainer discussion rather than a drive-by PR] +--- + statsig-pyo3/tests/test_forking.py | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/statsig-pyo3/tests/test_forking.py b/statsig-pyo3/tests/test_forking.py +index a5f27c4..8d2bd7d 100644 +--- a/statsig-pyo3/tests/test_forking.py ++++ b/statsig-pyo3/tests/test_forking.py +@@ -45,7 +45,7 @@ def test_forking(httpserver: HTTPServer): + env={**os.environ, "RUST_BACKTRACE": "full"}, + ) + try: +- proc.communicate(timeout=10) ++ proc.communicate(timeout=180) + except subprocess.TimeoutExpired: + proc.terminate() + proc.wait() diff --git a/patches/oai-statsig-python-core/0.34.0/0001-tests-wait-for-background-specs-syncs-instead-of-ass.patch b/patches/oai-statsig-python-core/0.34.0/0001-tests-wait-for-background-specs-syncs-instead-of-ass.patch new file mode 100644 index 00000000000..832832f3ec8 --- /dev/null +++ b/patches/oai-statsig-python-core/0.34.0/0001-tests-wait-for-background-specs-syncs-instead-of-ass.patch @@ -0,0 +1,156 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Ludovic Henry +Date: Sun, 20 Sep 2026 22:48:16 +0000 +Subject: [PATCH] tests: wait for background specs syncs instead of assuming a + 1ms interval + +`StatsigOptions` rejects a `specs_sync_interval_ms` below `MIN_SYNC_INTERVAL` +(1000) and drops it back to the default, logging "Invalid +'specs_sync_interval_ms', value must be greater than 1000, received Some(1)". +Both fixtures in this file still ask for 1, so the six polling tests never get +the near-instant background sync they were written against and fail on every +platform -- reproduced here against the published +manylinux_2_17_x86_64 wheel, where 6 of the 7 tests in this file fail. + +Use the smallest accepted interval -- the same 1000 upstream already switched +`test_observability_client.py` to -- and wait for the condition each test is +really about rather than for a fixed duration. The waits return as soon as the +sync lands, so fast hardware pays nothing. + +Upstream-Status: To upstream [not yet submitted; the release is cut from a non-public tree, so this needs a maintainer discussion rather than a drive-by PR] +--- + statsig-pyo3/tests/test_data_store.py | 46 +++++++++++++++++---------- + 1 file changed, 30 insertions(+), 16 deletions(-) + +diff --git a/statsig-pyo3/tests/test_data_store.py b/statsig-pyo3/tests/test_data_store.py +index 9df3ac7..708c81b 100644 +--- a/statsig-pyo3/tests/test_data_store.py ++++ b/statsig-pyo3/tests/test_data_store.py +@@ -17,6 +17,12 @@ from utils import get_test_data_resource, get_test_data_resource_bytes + + known_lcut = 1767981029384 + ++# StatsigOptions rejects a specs_sync_interval_ms below this and falls back to the ++# default, so the polling tests below have to wait for a real sync to land. ++MIN_SYNC_INTERVAL_MS = 1000 ++SYNC_WAIT_ATTEMPTS = 200 ++SYNC_WAIT_INTERVAL_S = 0.05 ++ + dcs_content = get_test_data_resource("eval_proj_dcs.json") + json_data = json.loads(dcs_content) + eval_proj_protobuf = get_test_data_resource_bytes("eval_proj_dcs.pb.br") +@@ -169,7 +175,7 @@ def statsig_setup(httpserver: HTTPServer): + specs_url=httpserver.url_for("/v2/download_config_specs"), + log_event_url=httpserver.url_for("/v1/log_event"), + data_store=data_store, +- specs_sync_interval_ms=1, ++ specs_sync_interval_ms=MIN_SYNC_INTERVAL_MS, + ) + + statsig = Statsig("secret-key", options) +@@ -193,7 +199,7 @@ def statsig_bytes_setup(httpserver: HTTPServer): + specs_url=httpserver.url_for("/v2/download_config_specs"), + log_event_url=httpserver.url_for("/v1/log_event"), + data_store=data_store, +- specs_sync_interval_ms=1, ++ specs_sync_interval_ms=MIN_SYNC_INTERVAL_MS, + ) + + statsig = Statsig("secret-key", options) +@@ -223,10 +229,10 @@ def test_data_store_usage_get(statsig_setup): + assert gate.value == True + assert gate.details.lcut == known_lcut + +- for _ in range(100): ++ for _ in range(SYNC_WAIT_ATTEMPTS): + if data_store.get_called_count >= 2: + break +- sleep(0.05) ++ sleep(SYNC_WAIT_INTERVAL_S) + + assert data_store.get_called_count > 1 + +@@ -240,9 +246,13 @@ def test_data_store_usage_set(statsig_setup): + + assert data_store.init_called + assert gate.details.reason == "Adapter(DataStore):Recognized" +- sleep(1) + +- gate_after = statsig.get_feature_gate(user, "test_public") ++ for _ in range(SYNC_WAIT_ATTEMPTS): ++ gate_after = statsig.get_feature_gate(user, "test_public") ++ if gate_after.details.lcut == known_lcut + 10: ++ break ++ sleep(SYNC_WAIT_INTERVAL_S) ++ + statsig.flush_events().wait() + + assert gate_after.value == True +@@ -265,10 +275,10 @@ def test_data_store_usage_get_bytes(statsig_bytes_setup): + assert gate.value == True + assert gate.details.lcut == known_lcut + +- for _ in range(5): ++ for _ in range(SYNC_WAIT_ATTEMPTS): + if data_store.set_bytes_called_count > 0: + break +- sleep(0.05) ++ sleep(SYNC_WAIT_INTERVAL_S) + + assert data_store.get_bytes_called_count >= 1 + assert data_store.get_called_count == 0 +@@ -283,10 +293,10 @@ def test_data_store_usage_get_bytes_request_has_since_time_after_initial_poll(st + gate = statsig.get_feature_gate(user, "test_public") + assert gate.details.reason == "Adapter(DataStore):Recognized" + +- for _ in range(100): ++ for _ in range(SYNC_WAIT_ATTEMPTS): + if data_store.get_bytes_called_count >= 2: + break +- sleep(0.05) ++ sleep(SYNC_WAIT_INTERVAL_S) + + assert data_store.get_bytes_called_count >= 2 + +@@ -308,10 +318,10 @@ def test_data_store_usage_get_bytes_request_checksum_match_returns_no_update(sta + gate = statsig.get_feature_gate(user, "test_public") + assert gate.details.reason == "Adapter(DataStore):Recognized" + +- for _ in range(100): ++ for _ in range(SYNC_WAIT_ATTEMPTS): + if data_store.get_bytes_called_count >= 2: + break +- sleep(0.05) ++ sleep(SYNC_WAIT_INTERVAL_S) + + assert data_store.get_bytes_called_count >= 2 + assert data_store.returned_no_update +@@ -325,10 +335,10 @@ def test_data_store_usage_get_bytes_request_checksum_match_returns_no_update(sta + assert request_with_checksum.since_time == data_store.stored_time + + # no second write should occur when server responds with {"has_updates": false} +- for _ in range(100): ++ for _ in range(SYNC_WAIT_ATTEMPTS): + if data_store.get_bytes_called_count > 2: + break +- sleep(0.05) ++ sleep(SYNC_WAIT_INTERVAL_S) + + + +@@ -341,9 +351,13 @@ def test_data_store_usage_set_bytes(statsig_bytes_setup): + + assert data_store.init_called + assert gate.details.reason == "Adapter(DataStore):Recognized" +- sleep(1) + +- gate_after = statsig.get_feature_gate(user, "test_public") ++ for _ in range(SYNC_WAIT_ATTEMPTS): ++ gate_after = statsig.get_feature_gate(user, "test_public") ++ if gate_after.details.lcut == known_lcut + 10: ++ break ++ sleep(SYNC_WAIT_INTERVAL_S) ++ + statsig.flush_events().wait() + + assert gate_after.value == True diff --git a/patches/oai-statsig-python-core/0.34.0/0002-tests-give-fork_runner.py-a-timeout-a-slow-machine-c.patch b/patches/oai-statsig-python-core/0.34.0/0002-tests-give-fork_runner.py-a-timeout-a-slow-machine-c.patch new file mode 100644 index 00000000000..82afe1f1a09 --- /dev/null +++ b/patches/oai-statsig-python-core/0.34.0/0002-tests-give-fork_runner.py-a-timeout-a-slow-machine-c.patch @@ -0,0 +1,32 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Ludovic Henry +Date: Sun, 20 Sep 2026 22:48:16 +0000 +Subject: [PATCH] tests: give fork_runner.py a timeout a slow machine can meet + +`fork_runner.py` performs 50 full SDK initialisations -- ten iterations, each +followed by four nested forks that initialise, evaluate and shut down -- against +a local mock server. Ten seconds is enough on upstream's x86_64 CI and not on a +riscv64 runner, where the subprocess is SIGTERMed part way through and the test +fails with `assert -15 == 0`. + +`communicate()` returns as soon as the subprocess exits, so a larger budget +costs nothing where the old one was already sufficient. + +Upstream-Status: To upstream [not yet submitted; the release is cut from a non-public tree, so this needs a maintainer discussion rather than a drive-by PR] +--- + statsig-pyo3/tests/test_forking.py | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/statsig-pyo3/tests/test_forking.py b/statsig-pyo3/tests/test_forking.py +index a5f27c4..8d2bd7d 100644 +--- a/statsig-pyo3/tests/test_forking.py ++++ b/statsig-pyo3/tests/test_forking.py +@@ -45,7 +45,7 @@ def test_forking(httpserver: HTTPServer): + env={**os.environ, "RUST_BACKTRACE": "full"}, + ) + try: +- proc.communicate(timeout=10) ++ proc.communicate(timeout=180) + except subprocess.TimeoutExpired: + proc.terminate() + proc.wait() diff --git a/patches/oai-statsig-python-core/0.35.0/0001-tests-wait-for-background-specs-syncs-instead-of-ass.patch b/patches/oai-statsig-python-core/0.35.0/0001-tests-wait-for-background-specs-syncs-instead-of-ass.patch new file mode 100644 index 00000000000..832832f3ec8 --- /dev/null +++ b/patches/oai-statsig-python-core/0.35.0/0001-tests-wait-for-background-specs-syncs-instead-of-ass.patch @@ -0,0 +1,156 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Ludovic Henry +Date: Sun, 20 Sep 2026 22:48:16 +0000 +Subject: [PATCH] tests: wait for background specs syncs instead of assuming a + 1ms interval + +`StatsigOptions` rejects a `specs_sync_interval_ms` below `MIN_SYNC_INTERVAL` +(1000) and drops it back to the default, logging "Invalid +'specs_sync_interval_ms', value must be greater than 1000, received Some(1)". +Both fixtures in this file still ask for 1, so the six polling tests never get +the near-instant background sync they were written against and fail on every +platform -- reproduced here against the published +manylinux_2_17_x86_64 wheel, where 6 of the 7 tests in this file fail. + +Use the smallest accepted interval -- the same 1000 upstream already switched +`test_observability_client.py` to -- and wait for the condition each test is +really about rather than for a fixed duration. The waits return as soon as the +sync lands, so fast hardware pays nothing. + +Upstream-Status: To upstream [not yet submitted; the release is cut from a non-public tree, so this needs a maintainer discussion rather than a drive-by PR] +--- + statsig-pyo3/tests/test_data_store.py | 46 +++++++++++++++++---------- + 1 file changed, 30 insertions(+), 16 deletions(-) + +diff --git a/statsig-pyo3/tests/test_data_store.py b/statsig-pyo3/tests/test_data_store.py +index 9df3ac7..708c81b 100644 +--- a/statsig-pyo3/tests/test_data_store.py ++++ b/statsig-pyo3/tests/test_data_store.py +@@ -17,6 +17,12 @@ from utils import get_test_data_resource, get_test_data_resource_bytes + + known_lcut = 1767981029384 + ++# StatsigOptions rejects a specs_sync_interval_ms below this and falls back to the ++# default, so the polling tests below have to wait for a real sync to land. ++MIN_SYNC_INTERVAL_MS = 1000 ++SYNC_WAIT_ATTEMPTS = 200 ++SYNC_WAIT_INTERVAL_S = 0.05 ++ + dcs_content = get_test_data_resource("eval_proj_dcs.json") + json_data = json.loads(dcs_content) + eval_proj_protobuf = get_test_data_resource_bytes("eval_proj_dcs.pb.br") +@@ -169,7 +175,7 @@ def statsig_setup(httpserver: HTTPServer): + specs_url=httpserver.url_for("/v2/download_config_specs"), + log_event_url=httpserver.url_for("/v1/log_event"), + data_store=data_store, +- specs_sync_interval_ms=1, ++ specs_sync_interval_ms=MIN_SYNC_INTERVAL_MS, + ) + + statsig = Statsig("secret-key", options) +@@ -193,7 +199,7 @@ def statsig_bytes_setup(httpserver: HTTPServer): + specs_url=httpserver.url_for("/v2/download_config_specs"), + log_event_url=httpserver.url_for("/v1/log_event"), + data_store=data_store, +- specs_sync_interval_ms=1, ++ specs_sync_interval_ms=MIN_SYNC_INTERVAL_MS, + ) + + statsig = Statsig("secret-key", options) +@@ -223,10 +229,10 @@ def test_data_store_usage_get(statsig_setup): + assert gate.value == True + assert gate.details.lcut == known_lcut + +- for _ in range(100): ++ for _ in range(SYNC_WAIT_ATTEMPTS): + if data_store.get_called_count >= 2: + break +- sleep(0.05) ++ sleep(SYNC_WAIT_INTERVAL_S) + + assert data_store.get_called_count > 1 + +@@ -240,9 +246,13 @@ def test_data_store_usage_set(statsig_setup): + + assert data_store.init_called + assert gate.details.reason == "Adapter(DataStore):Recognized" +- sleep(1) + +- gate_after = statsig.get_feature_gate(user, "test_public") ++ for _ in range(SYNC_WAIT_ATTEMPTS): ++ gate_after = statsig.get_feature_gate(user, "test_public") ++ if gate_after.details.lcut == known_lcut + 10: ++ break ++ sleep(SYNC_WAIT_INTERVAL_S) ++ + statsig.flush_events().wait() + + assert gate_after.value == True +@@ -265,10 +275,10 @@ def test_data_store_usage_get_bytes(statsig_bytes_setup): + assert gate.value == True + assert gate.details.lcut == known_lcut + +- for _ in range(5): ++ for _ in range(SYNC_WAIT_ATTEMPTS): + if data_store.set_bytes_called_count > 0: + break +- sleep(0.05) ++ sleep(SYNC_WAIT_INTERVAL_S) + + assert data_store.get_bytes_called_count >= 1 + assert data_store.get_called_count == 0 +@@ -283,10 +293,10 @@ def test_data_store_usage_get_bytes_request_has_since_time_after_initial_poll(st + gate = statsig.get_feature_gate(user, "test_public") + assert gate.details.reason == "Adapter(DataStore):Recognized" + +- for _ in range(100): ++ for _ in range(SYNC_WAIT_ATTEMPTS): + if data_store.get_bytes_called_count >= 2: + break +- sleep(0.05) ++ sleep(SYNC_WAIT_INTERVAL_S) + + assert data_store.get_bytes_called_count >= 2 + +@@ -308,10 +318,10 @@ def test_data_store_usage_get_bytes_request_checksum_match_returns_no_update(sta + gate = statsig.get_feature_gate(user, "test_public") + assert gate.details.reason == "Adapter(DataStore):Recognized" + +- for _ in range(100): ++ for _ in range(SYNC_WAIT_ATTEMPTS): + if data_store.get_bytes_called_count >= 2: + break +- sleep(0.05) ++ sleep(SYNC_WAIT_INTERVAL_S) + + assert data_store.get_bytes_called_count >= 2 + assert data_store.returned_no_update +@@ -325,10 +335,10 @@ def test_data_store_usage_get_bytes_request_checksum_match_returns_no_update(sta + assert request_with_checksum.since_time == data_store.stored_time + + # no second write should occur when server responds with {"has_updates": false} +- for _ in range(100): ++ for _ in range(SYNC_WAIT_ATTEMPTS): + if data_store.get_bytes_called_count > 2: + break +- sleep(0.05) ++ sleep(SYNC_WAIT_INTERVAL_S) + + + +@@ -341,9 +351,13 @@ def test_data_store_usage_set_bytes(statsig_bytes_setup): + + assert data_store.init_called + assert gate.details.reason == "Adapter(DataStore):Recognized" +- sleep(1) + +- gate_after = statsig.get_feature_gate(user, "test_public") ++ for _ in range(SYNC_WAIT_ATTEMPTS): ++ gate_after = statsig.get_feature_gate(user, "test_public") ++ if gate_after.details.lcut == known_lcut + 10: ++ break ++ sleep(SYNC_WAIT_INTERVAL_S) ++ + statsig.flush_events().wait() + + assert gate_after.value == True diff --git a/patches/oai-statsig-python-core/0.35.0/0002-tests-give-fork_runner.py-a-timeout-a-slow-machine-c.patch b/patches/oai-statsig-python-core/0.35.0/0002-tests-give-fork_runner.py-a-timeout-a-slow-machine-c.patch new file mode 100644 index 00000000000..82afe1f1a09 --- /dev/null +++ b/patches/oai-statsig-python-core/0.35.0/0002-tests-give-fork_runner.py-a-timeout-a-slow-machine-c.patch @@ -0,0 +1,32 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Ludovic Henry +Date: Sun, 20 Sep 2026 22:48:16 +0000 +Subject: [PATCH] tests: give fork_runner.py a timeout a slow machine can meet + +`fork_runner.py` performs 50 full SDK initialisations -- ten iterations, each +followed by four nested forks that initialise, evaluate and shut down -- against +a local mock server. Ten seconds is enough on upstream's x86_64 CI and not on a +riscv64 runner, where the subprocess is SIGTERMed part way through and the test +fails with `assert -15 == 0`. + +`communicate()` returns as soon as the subprocess exits, so a larger budget +costs nothing where the old one was already sufficient. + +Upstream-Status: To upstream [not yet submitted; the release is cut from a non-public tree, so this needs a maintainer discussion rather than a drive-by PR] +--- + statsig-pyo3/tests/test_forking.py | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/statsig-pyo3/tests/test_forking.py b/statsig-pyo3/tests/test_forking.py +index a5f27c4..8d2bd7d 100644 +--- a/statsig-pyo3/tests/test_forking.py ++++ b/statsig-pyo3/tests/test_forking.py +@@ -45,7 +45,7 @@ def test_forking(httpserver: HTTPServer): + env={**os.environ, "RUST_BACKTRACE": "full"}, + ) + try: +- proc.communicate(timeout=10) ++ proc.communicate(timeout=180) + except subprocess.TimeoutExpired: + proc.terminate() + proc.wait() From bd0d51e17cb0e1717342789830c6186dee50fb87 Mon Sep 17 00:00:00 2001 From: Ludovic Henry Date: Sat, 10 Oct 2026 07:45:56 +0000 Subject: [PATCH 3/3] oai-statsig-python-core: install the typed extra for the tests From 0.32.0 on, statsig-pyo3/tests/test_typed_config.py and test_typed_config_native.py import google.protobuf, so pytest collection fails with ModuleNotFoundError. Install the package's own `typed` extra (protobuf>=6.33.6), which resolves to protobuf's pure-Python wheel on riscv64. --- .github/workflows/build-oai-statsig-python-core.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/build-oai-statsig-python-core.yml b/.github/workflows/build-oai-statsig-python-core.yml index 76d17ce843c..d9bd0519c9f 100644 --- a/.github/workflows/build-oai-statsig-python-core.yml +++ b/.github/workflows/build-oai-statsig-python-core.yml @@ -143,6 +143,9 @@ jobs: # tests/mock_scrapi.py imports zstandard from 0.31.0 on. Neither PyPI nor our index # has a cp310 riscv64 wheel of it, so pip builds its sdist (bundled libzstd, plain C). CIBW_TEST_REQUIRES: pytest pytest-httpserver pytest-rerunfailures uvloop zstandard + # From 0.32.0 on, tests/test_typed_config*.py import google.protobuf, which the + # `typed` extra (protobuf>=6.33.6) pulls in. + CIBW_TEST_EXTRAS: typed CIBW_TEST_COMMAND: >- python -c "from statsig_python_core import statsig_python_core as m; assert m.__file__.endswith('.so'), m.__file__;