nvda-addon-testkit 0.1.3__tar.gz → 1.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.0.0}/PKG-INFO +13 -1
- {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.0.0}/README.md +12 -0
- {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.0.0}/docs/modules/ROOT/examples/tests_e2e/conftest.py +6 -0
- {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.0.0}/docs/modules/ROOT/examples/tests_e2e/test_demo_addon.py +5 -5
- nvda_addon_testkit-1.0.0/docs/modules/ROOT/examples/tests_e2e/test_modal_dialog_investigation.py +67 -0
- {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.0.0}/docs/modules/ROOT/examples/tests_e2e/test_smoke.py +8 -1
- {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.0.0}/docs/modules/ROOT/pages/troubleshooting.adoc +1 -1
- nvda_addon_testkit-1.0.0/spy/globalPlugins/nvda_testkit_spy/eval_api.py +52 -0
- {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.0.0}/spy/globalPlugins/nvda_testkit_spy/mainthread.py +11 -2
- {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.0.0}/spy/globalPlugins/nvda_testkit_spy/registry.py +3 -1
- {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.0.0}/src/nvda_testkit/_spy/nvda-testkit-spy.nvda-addon +0 -0
- {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.0.0}/src/nvda_testkit/_version.py +2 -2
- {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.0.0}/src/nvda_testkit/client.py +44 -2
- {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.0.0}/src/nvda_testkit/errors.py +14 -0
- {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.0.0}/src/nvda_testkit/namespaces/addons.py +1 -1
- {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.0.0}/src/nvda_testkit/plugin.py +2 -2
- {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.0.0}/src/nvda_testkit/process.py +55 -1
- {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.0.0}/src/nvda_testkit/rpcclient.py +11 -4
- {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.0.0}/tests/fake_nvda.py +5 -0
- nvda_addon_testkit-1.0.0/tests/test_client.py +312 -0
- {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.0.0}/tests/test_process.py +113 -0
- {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.0.0}/tests/test_rpcclient.py +23 -1
- {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.0.0}/tests_spy/test_eval_api.py +38 -0
- {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.0.0}/tests_spy/test_mainthread.py +34 -0
- nvda_addon_testkit-0.1.3/spy/globalPlugins/nvda_testkit_spy/eval_api.py +0 -35
- nvda_addon_testkit-0.1.3/tests/test_client.py +0 -123
- {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.0.0}/.github/CODEOWNERS +0 -0
- {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.0.0}/.github/CONTRIBUTING.md +0 -0
- {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.0.0}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
- {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.0.0}/.github/ISSUE_TEMPLATE/config.yml +0 -0
- {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.0.0}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
- {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.0.0}/.github/pull_request_template.md +0 -0
- {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.0.0}/.github/renovate.json +0 -0
- {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.0.0}/.github/workflows/CI.yml +0 -0
- {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.0.0}/.github/workflows/notify-docs.yml +0 -0
- {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.0.0}/.github/workflows/prepare-release.yml +0 -0
- {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.0.0}/.github/workflows/project-sync.yml +0 -0
- {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.0.0}/.github/workflows/publish-python.yml +0 -0
- {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.0.0}/.github/workflows/sonar-fork-coverage.yml +0 -0
- {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.0.0}/.github/workflows/sonar-fork-scan.yml +0 -0
- {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.0.0}/.github/workflows/sonar.yml +0 -0
- {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.0.0}/.github/workflows/stale.yml +0 -0
- {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.0.0}/.github/workflows/zizmor.yml +0 -0
- {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.0.0}/.gitignore +0 -0
- {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.0.0}/.sonarlint/connectedMode.json +0 -0
- {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.0.0}/LICENSE +0 -0
- {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.0.0}/action.yml +0 -0
- {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.0.0}/docs/antora.yml +0 -0
- {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.0.0}/docs/modules/ROOT/examples/demo-addon/build.py +0 -0
- {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.0.0}/docs/modules/ROOT/examples/demo-addon/globalPlugins/testkit_demo.py +0 -0
- {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.0.0}/docs/modules/ROOT/examples/demo-addon/manifest.ini +0 -0
- {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.0.0}/docs/modules/ROOT/nav.adoc +0 -0
- {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.0.0}/docs/modules/ROOT/pages/ci-guide.adoc +0 -0
- {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.0.0}/docs/modules/ROOT/pages/contributing.adoc +0 -0
- {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.0.0}/docs/modules/ROOT/pages/example-project.adoc +0 -0
- {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.0.0}/docs/modules/ROOT/pages/guide/addons.adoc +0 -0
- {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.0.0}/docs/modules/ROOT/pages/guide/braille.adoc +0 -0
- {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.0.0}/docs/modules/ROOT/pages/guide/config.adoc +0 -0
- {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.0.0}/docs/modules/ROOT/pages/guide/fixtures.adoc +0 -0
- {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.0.0}/docs/modules/ROOT/pages/guide/keys.adoc +0 -0
- {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.0.0}/docs/modules/ROOT/pages/guide/log.adoc +0 -0
- {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.0.0}/docs/modules/ROOT/pages/guide/speech.adoc +0 -0
- {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.0.0}/docs/modules/ROOT/pages/index.adoc +0 -0
- {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.0.0}/docs/modules/ROOT/pages/installation.adoc +0 -0
- {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.0.0}/docs/modules/ROOT/pages/tutorial.adoc +0 -0
- {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.0.0}/pyproject.toml +0 -0
- {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.0.0}/ruff.toml +0 -0
- {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.0.0}/scripts/next-version.sh +0 -0
- {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.0.0}/sonar-project.properties +0 -0
- {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.0.0}/spy/globalPlugins/nvda_testkit_spy/__init__.py +0 -0
- {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.0.0}/spy/globalPlugins/nvda_testkit_spy/addons_api.py +0 -0
- {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.0.0}/spy/globalPlugins/nvda_testkit_spy/braille_tap.py +0 -0
- {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.0.0}/spy/globalPlugins/nvda_testkit_spy/config_api.py +0 -0
- {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.0.0}/spy/globalPlugins/nvda_testkit_spy/input_api.py +0 -0
- {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.0.0}/spy/globalPlugins/nvda_testkit_spy/log_tap.py +0 -0
- {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.0.0}/spy/globalPlugins/nvda_testkit_spy/serialise.py +0 -0
- {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.0.0}/spy/globalPlugins/nvda_testkit_spy/server.py +0 -0
- {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.0.0}/spy/globalPlugins/nvda_testkit_spy/speech_tap.py +0 -0
- {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.0.0}/spy/manifest.ini +0 -0
- {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.0.0}/src/nvda_testkit/__init__.py +0 -0
- {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.0.0}/src/nvda_testkit/cli.py +0 -0
- {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.0.0}/src/nvda_testkit/download.py +0 -0
- {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.0.0}/src/nvda_testkit/namespaces/__init__.py +0 -0
- {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.0.0}/src/nvda_testkit/namespaces/braille.py +0 -0
- {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.0.0}/src/nvda_testkit/namespaces/config.py +0 -0
- {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.0.0}/src/nvda_testkit/namespaces/keys.py +0 -0
- {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.0.0}/src/nvda_testkit/namespaces/log.py +0 -0
- {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.0.0}/src/nvda_testkit/namespaces/speech.py +0 -0
- {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.0.0}/src/nvda_testkit/portable.py +0 -0
- {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.0.0}/src/nvda_testkit/provisioning.py +0 -0
- {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.0.0}/src/nvda_testkit/resolve.py +0 -0
- {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.0.0}/src/nvda_testkit/settings.py +0 -0
- {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.0.0}/src/nvda_testkit/speechtypes.py +0 -0
- {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.0.0}/src/nvda_testkit/spybundle.py +0 -0
- {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.0.0}/tests/conftest.py +0 -0
- {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.0.0}/tests/fixtures/snapshot_index_alpha.html +0 -0
- {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.0.0}/tests/fixtures/update_check_stable.txt +0 -0
- {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.0.0}/tests/test_addons_namespace.py +0 -0
- {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.0.0}/tests/test_braille_namespace.py +0 -0
- {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.0.0}/tests/test_build_spy.py +0 -0
- {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.0.0}/tests/test_cli.py +0 -0
- {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.0.0}/tests/test_config_namespace.py +0 -0
- {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.0.0}/tests/test_download.py +0 -0
- {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.0.0}/tests/test_e2e_conftest.py +0 -0
- {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.0.0}/tests/test_errors.py +0 -0
- {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.0.0}/tests/test_fake_nvda.py +0 -0
- {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.0.0}/tests/test_keys_namespace.py +0 -0
- {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.0.0}/tests/test_log_namespace.py +0 -0
- {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.0.0}/tests/test_plugin.py +0 -0
- {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.0.0}/tests/test_portable.py +0 -0
- {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.0.0}/tests/test_provisioning.py +0 -0
- {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.0.0}/tests/test_resolve.py +0 -0
- {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.0.0}/tests/test_settings.py +0 -0
- {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.0.0}/tests/test_speech_namespace.py +0 -0
- {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.0.0}/tests/test_speechtypes.py +0 -0
- {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.0.0}/tests_spy/__init__.py +0 -0
- {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.0.0}/tests_spy/conftest.py +0 -0
- {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.0.0}/tests_spy/nvda_stubs.py +0 -0
- {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.0.0}/tests_spy/test_addons_api.py +0 -0
- {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.0.0}/tests_spy/test_braille_tap.py +0 -0
- {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.0.0}/tests_spy/test_config_api.py +0 -0
- {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.0.0}/tests_spy/test_input_api.py +0 -0
- {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.0.0}/tests_spy/test_log_tap.py +0 -0
- {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.0.0}/tests_spy/test_plugin.py +0 -0
- {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.0.0}/tests_spy/test_registry.py +0 -0
- {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.0.0}/tests_spy/test_serialise.py +0 -0
- {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.0.0}/tests_spy/test_server.py +0 -0
- {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.0.0}/tests_spy/test_speech_tap.py +0 -0
- {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.0.0}/tools/build_spy.py +0 -0
- {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.0.0}/uv.lock +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: nvda-addon-testkit
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 1.0.0
|
|
4
4
|
Summary: End-to-end testing for NVDA add-ons against a real NVDA
|
|
5
5
|
Project-URL: Homepage, https://github.com/ZirekHQ/nvda-addon-testkit
|
|
6
6
|
Project-URL: Issues, https://github.com/ZirekHQ/nvda-addon-testkit/issues
|
|
@@ -94,6 +94,18 @@ still on Extended Security Updates).
|
|
|
94
94
|
| `nvda.log` | structured log records, and `assert_no_errors()` |
|
|
95
95
|
| `nvda.addons` | two-phase install, remove, and state |
|
|
96
96
|
|
|
97
|
+
`nvda.eval()` runs a single expression inside NVDA; `nvda.exec()` runs a
|
|
98
|
+
full multi-statement scenario and returns whatever it binds to
|
|
99
|
+
`__result__`. Both need `--nvda-allow-eval`. A bad scenario raises
|
|
100
|
+
`ScenarioSyntaxError`, so catching bare `except Exception: pass` around
|
|
101
|
+
either call still swallows it — catch the types you expect instead.
|
|
102
|
+
|
|
103
|
+
`nvda.restart_harness()` kills and relaunches the NVDA process — use it to
|
|
104
|
+
finish a two-phase add-on install or reset to a clean process. It does not
|
|
105
|
+
exercise NVDA's own restart logic. For that, use `nvda.restart_nvda()`,
|
|
106
|
+
which triggers NVDA's real `core.restart()` and waits for the replacement
|
|
107
|
+
process — needs `--nvda-allow-eval`, since it is built on `nvda.eval()`.
|
|
108
|
+
|
|
97
109
|
## Requirements
|
|
98
110
|
|
|
99
111
|
Windows to run the tests. NVDA is downloaded automatically — you do not need
|
|
@@ -67,6 +67,18 @@ still on Extended Security Updates).
|
|
|
67
67
|
| `nvda.log` | structured log records, and `assert_no_errors()` |
|
|
68
68
|
| `nvda.addons` | two-phase install, remove, and state |
|
|
69
69
|
|
|
70
|
+
`nvda.eval()` runs a single expression inside NVDA; `nvda.exec()` runs a
|
|
71
|
+
full multi-statement scenario and returns whatever it binds to
|
|
72
|
+
`__result__`. Both need `--nvda-allow-eval`. A bad scenario raises
|
|
73
|
+
`ScenarioSyntaxError`, so catching bare `except Exception: pass` around
|
|
74
|
+
either call still swallows it — catch the types you expect instead.
|
|
75
|
+
|
|
76
|
+
`nvda.restart_harness()` kills and relaunches the NVDA process — use it to
|
|
77
|
+
finish a two-phase add-on install or reset to a clean process. It does not
|
|
78
|
+
exercise NVDA's own restart logic. For that, use `nvda.restart_nvda()`,
|
|
79
|
+
which triggers NVDA's real `core.restart()` and waits for the replacement
|
|
80
|
+
process — needs `--nvda-allow-eval`, since it is built on `nvda.eval()`.
|
|
81
|
+
|
|
70
82
|
## Requirements
|
|
71
83
|
|
|
72
84
|
Windows to run the tests. NVDA is downloaded automatically — you do not need
|
|
@@ -65,3 +65,9 @@ def check_no_unexpected_errors(client, *, since: int = 0) -> None:
|
|
|
65
65
|
@pytest.fixture
|
|
66
66
|
def assert_no_unexpected_errors():
|
|
67
67
|
return check_no_unexpected_errors
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@pytest.fixture
|
|
71
|
+
def require_eval(pytestconfig):
|
|
72
|
+
if not pytestconfig.option.nvda_allow_eval:
|
|
73
|
+
pytest.skip("needs --nvda-allow-eval")
|
|
@@ -27,19 +27,19 @@ def test_install_is_two_phase_and_completes_on_restart(
|
|
|
27
27
|
assert info.name == "testkit-demo"
|
|
28
28
|
assert nvda.addons.state("testkit-demo") is AddonState.PENDING_INSTALL
|
|
29
29
|
|
|
30
|
-
nvda.
|
|
30
|
+
nvda.restart_harness()
|
|
31
31
|
assert nvda.addons.state("testkit-demo") is AddonState.ENABLED
|
|
32
32
|
assert_no_unexpected_errors(nvda)
|
|
33
33
|
|
|
34
34
|
nvda.addons.remove("testkit-demo")
|
|
35
|
-
nvda.
|
|
35
|
+
nvda.restart_harness()
|
|
36
36
|
assert nvda.addons.state("testkit-demo") is AddonState.NOT_INSTALLED
|
|
37
37
|
# end::addons[]
|
|
38
38
|
|
|
39
39
|
|
|
40
40
|
def test_the_installed_addon_logs_at_startup(nvda, addon_under_test):
|
|
41
41
|
# tag::log[]
|
|
42
|
-
nvda.
|
|
42
|
+
nvda.restart_harness()
|
|
43
43
|
nvda.log.wait_for(re.escape(STARTUP_MESSAGE), since=0, timeout=20)
|
|
44
44
|
# end::log[]
|
|
45
45
|
|
|
@@ -53,7 +53,7 @@ def test_its_gesture_produces_the_expected_speech(nvda, addon_under_test):
|
|
|
53
53
|
|
|
54
54
|
def test_it_survives_a_restart(nvda, addon_under_test):
|
|
55
55
|
# tag::fixtures[]
|
|
56
|
-
nvda.
|
|
56
|
+
nvda.restart_harness()
|
|
57
57
|
assert nvda.addons.state("testkit-demo") is AddonState.ENABLED
|
|
58
58
|
before = nvda.speech.index()
|
|
59
59
|
nvda.keys.press("NVDA+shift+control+d")
|
|
@@ -66,5 +66,5 @@ def test_removal_is_also_two_phase(nvda, addon_under_test):
|
|
|
66
66
|
and that session-scoped fixture will not reinstall it."""
|
|
67
67
|
nvda.addons.remove("testkit-demo")
|
|
68
68
|
assert nvda.addons.state("testkit-demo") is AddonState.PENDING_REMOVE
|
|
69
|
-
nvda.
|
|
69
|
+
nvda.restart_harness()
|
|
70
70
|
assert nvda.addons.state("testkit-demo") is AddonState.NOT_INSTALLED
|
nvda_addon_testkit-1.0.0/docs/modules/ROOT/examples/tests_e2e/test_modal_dialog_investigation.py
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""Investigation for issue #34, gaps 2 & 5: does a queued main-thread job
|
|
2
|
+
start while a real ShowModal() dialog is up?
|
|
3
|
+
|
|
4
|
+
This is not a regression test. It is a one-shot probe: run it once on
|
|
5
|
+
Windows CI, read the result, and act on it per the plan (either open a new
|
|
6
|
+
issue describing a real fix, or proceed with the simulate_modal() fallback
|
|
7
|
+
in the next task). Delete or keep this file once the investigation is
|
|
8
|
+
resolved -- it is not meant to run on every CI build.
|
|
9
|
+
|
|
10
|
+
Everything happens inside a single exec_in_nvda call, entirely on NVDA's
|
|
11
|
+
main thread: the spy's XML-RPC server (server.py) is a plain, unthreaded
|
|
12
|
+
SimpleXMLRPCServer, so two separate RPC connections can never be genuinely
|
|
13
|
+
concurrent at the transport level -- the second call simply can't be
|
|
14
|
+
dispatched until the first's handler returns. Queuing the second job from
|
|
15
|
+
*inside* the running scenario, via queueHandler.queueFunction directly,
|
|
16
|
+
avoids needing RPC-level concurrency at all: it tests whether NVDA's own
|
|
17
|
+
queue-draining mechanism still runs while the main thread is nested inside
|
|
18
|
+
ShowModal()'s event loop, using a timestamp comparison instead of a second
|
|
19
|
+
network round trip.
|
|
20
|
+
|
|
21
|
+
A third, lower-probability outcome is possible: if the dialog's own
|
|
22
|
+
wx.CallLater dismiss timer never fires for some unrelated reason, this
|
|
23
|
+
scenario never returns from ShowModal(), so nvda.exec() raises an RpcError
|
|
24
|
+
(wrapping exec_in_nvda's server-side "started on NVDA's main thread but did
|
|
25
|
+
not return within 30.0s" timeout) instead of returning a __result__ to
|
|
26
|
+
assert on. If you see that on a real run, it's worth investigating
|
|
27
|
+
separately -- it doesn't confirm or refute the queue-blocking hypothesis
|
|
28
|
+
either way.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def test_a_second_job_while_a_real_modal_is_up(require_eval, nvda):
|
|
33
|
+
scenario = (
|
|
34
|
+
"import queueHandler\n"
|
|
35
|
+
"import time\n"
|
|
36
|
+
"import wx\n"
|
|
37
|
+
"job_b_ran_at = []\n"
|
|
38
|
+
# exec_in_nvda runs this with separate globals/locals dicts (like a
|
|
39
|
+
# class body), so a nested def can't see job_b_ran_at/time as
|
|
40
|
+
# globals -- bind both as defaults, evaluated now, in this scope.
|
|
41
|
+
"def job_b(sink=job_b_ran_at, now=time.monotonic):\n"
|
|
42
|
+
" sink.append(now())\n"
|
|
43
|
+
"queueHandler.queueFunction(queueHandler.eventQueue, job_b)\n"
|
|
44
|
+
"dlg = wx.MessageDialog(None, 'probe', 'probe', wx.YES_NO)\n"
|
|
45
|
+
"wx.CallLater(1000, dlg.EndModal, wx.ID_YES)\n"
|
|
46
|
+
"before = time.monotonic()\n"
|
|
47
|
+
"dlg.ShowModal()\n"
|
|
48
|
+
"after = time.monotonic()\n"
|
|
49
|
+
"dlg.Destroy()\n"
|
|
50
|
+
"__result__ = {\n"
|
|
51
|
+
" 'ran_during_modal': bool(job_b_ran_at) and before <= job_b_ran_at[0] <= after,\n"
|
|
52
|
+
" 'ran_at_all': bool(job_b_ran_at),\n"
|
|
53
|
+
"}\n"
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
result = nvda.exec(scenario)
|
|
57
|
+
|
|
58
|
+
assert result["ran_during_modal"], (
|
|
59
|
+
"CONFIRMS THE DEADLOCK: a second main-thread job never ran while a "
|
|
60
|
+
f"real ShowModal() dialog was up (ran_at_all={result['ran_at_all']!r}). "
|
|
61
|
+
"Proceed with Task 7 (the simulate_modal() fallback)."
|
|
62
|
+
)
|
|
63
|
+
# If this assertion passes instead, the queue IS drained during
|
|
64
|
+
# ShowModal() in this NVDA version: STOP, do not proceed to Task 7, and
|
|
65
|
+
# open a new issue describing this finding plus what actually blocks
|
|
66
|
+
# input_api.py's keys_press() from dismissing the dialog (if anything
|
|
67
|
+
# still does).
|
|
@@ -37,5 +37,12 @@ def test_config_round_trips_through_a_real_nvda(nvda):
|
|
|
37
37
|
|
|
38
38
|
|
|
39
39
|
def test_startup_produced_no_errors(nvda, assert_no_unexpected_errors):
|
|
40
|
-
nvda.
|
|
40
|
+
nvda.restart_harness()
|
|
41
|
+
assert_no_unexpected_errors(nvda)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def test_restart_nvda_exercises_core_restart(require_eval, nvda, assert_no_unexpected_errors):
|
|
45
|
+
old_pid = nvda.process.handshake.pid
|
|
46
|
+
nvda.restart_nvda(timeout=60)
|
|
47
|
+
assert nvda.process.handshake.pid != old_pid
|
|
41
48
|
assert_no_unexpected_errors(nvda)
|
{nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.0.0}/docs/modules/ROOT/pages/troubleshooting.adoc
RENAMED
|
@@ -63,7 +63,7 @@ the current one; clean it out or narrow the pattern.
|
|
|
63
63
|
fails.
|
|
64
64
|
|
|
65
65
|
Install is two-phase by design — `install()` only reaches
|
|
66
|
-
`PENDING_INSTALL`; a call to `nvda.
|
|
66
|
+
`PENDING_INSTALL`; a call to `nvda.restart_harness()` is what completes it. See
|
|
67
67
|
xref:guide/addons.adoc[].
|
|
68
68
|
|
|
69
69
|
== Speech assertion mismatches
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# coding: utf-8
|
|
2
|
+
"""Run code inside NVDA's own process: one expression, or a scenario.
|
|
3
|
+
|
|
4
|
+
The host refuses to call either unless the session opted in, so the spy does
|
|
5
|
+
not second-guess it: the point is to reach NVDA's live state, which means
|
|
6
|
+
full builtins and real imports. Both run on the main thread for the same
|
|
7
|
+
reason every other mutation does.
|
|
8
|
+
|
|
9
|
+
eval_in_nvda evaluates a single expression and returns its value.
|
|
10
|
+
exec_in_nvda runs one or more statements and returns whatever the code bound
|
|
11
|
+
to a name called __result__, or None if it bound nothing -- multi-statement
|
|
12
|
+
scenarios (e.g. "import core; core.restart()") do not compile under eval()
|
|
13
|
+
and previously needed an unreadable immediately-invoked-lambda workaround.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import builtins
|
|
17
|
+
|
|
18
|
+
from .mainthread import run_on_main_thread
|
|
19
|
+
from .registry import rpc_method
|
|
20
|
+
|
|
21
|
+
_SCALARS = (str, int, float, bool, type(None))
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _marshallable(value):
|
|
25
|
+
"""xmlrpc carries scalars and containers of scalars; everything else is a repr."""
|
|
26
|
+
if isinstance(value, _SCALARS):
|
|
27
|
+
return value
|
|
28
|
+
if isinstance(value, dict):
|
|
29
|
+
return {str(key): _marshallable(item) for key, item in value.items()}
|
|
30
|
+
if isinstance(value, (list, tuple, set, frozenset)):
|
|
31
|
+
return [_marshallable(item) for item in value]
|
|
32
|
+
return repr(value)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _evaluate(source):
|
|
36
|
+
return eval(source, {"__builtins__": builtins})
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@rpc_method
|
|
40
|
+
def eval_in_nvda(source, timeout=30.0):
|
|
41
|
+
return _marshallable(run_on_main_thread(lambda: _evaluate(source), timeout=timeout))
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _execute(source):
|
|
45
|
+
scope = {"__builtins__": builtins}
|
|
46
|
+
exec(compile(source, "<nvda-testkit>", "exec"), scope)
|
|
47
|
+
return scope.get("__result__")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@rpc_method
|
|
51
|
+
def exec_in_nvda(source, timeout=30.0):
|
|
52
|
+
return _marshallable(run_on_main_thread(lambda: _execute(source), timeout=timeout))
|
|
@@ -24,8 +24,10 @@ def run_on_main_thread(fn, timeout=DEFAULT_TIMEOUT):
|
|
|
24
24
|
outcome = {"value": _MISSING, "error": None}
|
|
25
25
|
finished = threading.Event()
|
|
26
26
|
timed_out = threading.Event()
|
|
27
|
+
started = threading.Event()
|
|
27
28
|
|
|
28
29
|
def runner():
|
|
30
|
+
started.set()
|
|
29
31
|
try:
|
|
30
32
|
outcome["value"] = fn()
|
|
31
33
|
except BaseException as error: # NOSONAR -- forwarded verbatim, see raise below
|
|
@@ -38,9 +40,16 @@ def run_on_main_thread(fn, timeout=DEFAULT_TIMEOUT):
|
|
|
38
40
|
queueHandler.queueFunction(queueHandler.eventQueue, runner)
|
|
39
41
|
if not finished.wait(timeout):
|
|
40
42
|
timed_out.set()
|
|
43
|
+
if started.is_set():
|
|
44
|
+
raise TimeoutError(
|
|
45
|
+
"%r started on NVDA's main thread but did not return "
|
|
46
|
+
"within %.1fs. It's hung, not queued behind something else."
|
|
47
|
+
% (getattr(fn, "__name__", fn), timeout)
|
|
48
|
+
)
|
|
41
49
|
raise TimeoutError(
|
|
42
|
-
"
|
|
43
|
-
"
|
|
50
|
+
"%r never started on NVDA's main thread within %.1fs. "
|
|
51
|
+
"The queue is backed up or NVDA is unresponsive to "
|
|
52
|
+
"queueFunction()." % (getattr(fn, "__name__", fn), timeout)
|
|
44
53
|
)
|
|
45
54
|
if outcome["error"] is not None:
|
|
46
55
|
raise outcome["error"]
|
{nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.0.0}/spy/globalPlugins/nvda_testkit_spy/registry.py
RENAMED
|
@@ -22,7 +22,9 @@ class Dispatcher:
|
|
|
22
22
|
Faults are raised as plain exceptions; SimpleXMLRPCServer turns them into
|
|
23
23
|
Faults whose faultString is "<Type>: <message>". The host matches on the
|
|
24
24
|
"AUTH:" prefix, so those two strings are a wire contract -- do not reword
|
|
25
|
-
them without changing rpcclient.py.
|
|
25
|
+
them without changing rpcclient.py. The "<TypeName>: <message>" shape of
|
|
26
|
+
that faultString is part of the same contract: rpcclient.py reads the type
|
|
27
|
+
name back out of it to recognise a SyntaxError as ScenarioSyntaxError.
|
|
26
28
|
"""
|
|
27
29
|
|
|
28
30
|
def __init__(self, token):
|
|
Binary file
|
|
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
|
|
|
18
18
|
commit_id: str | None
|
|
19
19
|
__commit_id__: str | None
|
|
20
20
|
|
|
21
|
-
__version__ = version = '0.
|
|
22
|
-
__version_tuple__ = version_tuple = (
|
|
21
|
+
__version__ = version = '1.0.0'
|
|
22
|
+
__version_tuple__ = version_tuple = (1, 0, 0)
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
|
@@ -2,10 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import contextlib
|
|
5
6
|
from dataclasses import dataclass
|
|
6
7
|
from typing import Any
|
|
7
8
|
|
|
8
|
-
from .errors import TestkitError
|
|
9
|
+
from .errors import ConnectionLost, TestkitError
|
|
9
10
|
from .namespaces.addons import AddonsNamespace
|
|
10
11
|
from .namespaces.braille import BrailleNamespace
|
|
11
12
|
from .namespaces.config import ConfigNamespace
|
|
@@ -86,7 +87,9 @@ class NvdaClient:
|
|
|
86
87
|
if failures:
|
|
87
88
|
raise TestkitError("reset() failed for " + "; ".join(failures))
|
|
88
89
|
|
|
89
|
-
def
|
|
90
|
+
def restart_harness(self, *, timeout: float = 60.0) -> None:
|
|
91
|
+
"""Kill and relaunch the NVDA process. Does not exercise NVDA's own
|
|
92
|
+
core.restart() -- see restart_nvda() for that."""
|
|
90
93
|
handshake = self._process.restart(timeout=timeout)
|
|
91
94
|
self._rpc.close()
|
|
92
95
|
self._attach(
|
|
@@ -97,6 +100,36 @@ class NvdaClient:
|
|
|
97
100
|
)
|
|
98
101
|
)
|
|
99
102
|
|
|
103
|
+
def restart_nvda(self, *, timeout: float = 60.0) -> None:
|
|
104
|
+
"""Trigger NVDA's own core.restart() and wait for its replacement
|
|
105
|
+
process to announce itself. Requires allow_eval, since it is built
|
|
106
|
+
on eval() internally. Use this, not restart_harness(), to verify
|
|
107
|
+
behavior that lives in NVDA's real self-relaunch path."""
|
|
108
|
+
if not self._settings.allow_eval:
|
|
109
|
+
raise TestkitError(
|
|
110
|
+
"nvda.restart_nvda() is disabled. It runs arbitrary code inside NVDA, so it is "
|
|
111
|
+
"opt-in: pass --nvda-allow-eval, or set allow-eval = true under "
|
|
112
|
+
"[tool.nvda-testkit]."
|
|
113
|
+
)
|
|
114
|
+
handshake = self._process.handshake
|
|
115
|
+
if handshake is None:
|
|
116
|
+
raise TestkitError("NVDA is not running; nothing to restart.")
|
|
117
|
+
old_pid = handshake.pid
|
|
118
|
+
self._process.handshake_path.unlink(missing_ok=True)
|
|
119
|
+
with contextlib.suppress(ConnectionLost):
|
|
120
|
+
self.eval("__import__('core').restart()")
|
|
121
|
+
new_handshake = self._process.adopt_relaunched_handshake(
|
|
122
|
+
exclude_pid=old_pid, timeout=timeout
|
|
123
|
+
)
|
|
124
|
+
self._rpc.close()
|
|
125
|
+
self._attach(
|
|
126
|
+
RpcClient.from_handshake(
|
|
127
|
+
new_handshake,
|
|
128
|
+
token=self._process.token,
|
|
129
|
+
timeout_scale=self._settings.timeout_scale,
|
|
130
|
+
)
|
|
131
|
+
)
|
|
132
|
+
|
|
100
133
|
def eval(self, source: str) -> Any:
|
|
101
134
|
if not self._settings.allow_eval:
|
|
102
135
|
raise TestkitError(
|
|
@@ -106,5 +139,14 @@ class NvdaClient:
|
|
|
106
139
|
)
|
|
107
140
|
return self._rpc.call("eval_in_nvda", source)
|
|
108
141
|
|
|
142
|
+
def exec(self, source: str) -> Any:
|
|
143
|
+
if not self._settings.allow_eval:
|
|
144
|
+
raise TestkitError(
|
|
145
|
+
"nvda.exec() is disabled. It runs arbitrary code inside NVDA, so it is "
|
|
146
|
+
"opt-in: pass --nvda-allow-eval, or set allow-eval = true under "
|
|
147
|
+
"[tool.nvda-testkit]."
|
|
148
|
+
)
|
|
149
|
+
return self._rpc.call("exec_in_nvda", source)
|
|
150
|
+
|
|
109
151
|
def close(self) -> None:
|
|
110
152
|
self._rpc.close()
|
|
@@ -46,6 +46,20 @@ class AuthError(RpcError):
|
|
|
46
46
|
"""The spy rejected our token. Almost always a stale NVDA from a previous run."""
|
|
47
47
|
|
|
48
48
|
|
|
49
|
+
class ConnectionLost(RpcError):
|
|
50
|
+
"""The transport dropped before a response arrived -- expected when the
|
|
51
|
+
process answering the call is exiting (e.g. mid-restart)."""
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class ScenarioSyntaxError(RpcError):
|
|
55
|
+
"""`nvda.eval()`/`nvda.exec()` was given source that doesn't compile.
|
|
56
|
+
|
|
57
|
+
Distinguished from a plain RpcError so a scenario's own `except
|
|
58
|
+
Exception:` doesn't have to be the only thing standing between a typo
|
|
59
|
+
and a silently-skipped assertion.
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
|
|
49
63
|
class WaitTimeout(TestkitError):
|
|
50
64
|
"""A deadline-bounded poll expired before its predicate came true."""
|
|
51
65
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""nvda.addons -- add-on install lifecycle.
|
|
2
2
|
|
|
3
3
|
Installation is two-phase, and that is exposed rather than hidden: install()
|
|
4
|
-
leaves the add-on PENDING_INSTALL, and only nvda.
|
|
4
|
+
leaves the add-on PENDING_INSTALL, and only nvda.restart_harness() makes it ENABLED.
|
|
5
5
|
A test of installTasks.onInstall depends on being able to see both halves.
|
|
6
6
|
"""
|
|
7
7
|
|
|
@@ -78,7 +78,7 @@ def nvda_session(_nvda_provisioned, nvda_settings) -> NvdaClient:
|
|
|
78
78
|
@pytest.fixture
|
|
79
79
|
def nvda(request: pytest.FixtureRequest, nvda_session: NvdaClient) -> NvdaClient:
|
|
80
80
|
if request.node.get_closest_marker("fresh_nvda"):
|
|
81
|
-
nvda_session.
|
|
81
|
+
nvda_session.restart_harness()
|
|
82
82
|
nvda_session.reset()
|
|
83
83
|
return nvda_session
|
|
84
84
|
|
|
@@ -128,5 +128,5 @@ def addon_bundle(nvda_settings) -> Path:
|
|
|
128
128
|
def addon_under_test(nvda_session: NvdaClient, addon_bundle: Path) -> Path:
|
|
129
129
|
"""The bundle, installed and enabled, with NVDA restarted to complete it."""
|
|
130
130
|
nvda_session.addons.install(addon_bundle)
|
|
131
|
-
nvda_session.
|
|
131
|
+
nvda_session.restart_harness()
|
|
132
132
|
return addon_bundle
|
|
@@ -97,6 +97,7 @@ class NvdaProcess:
|
|
|
97
97
|
self._extra_env = dict(env or {})
|
|
98
98
|
self._proc: subprocess.Popen | None = None
|
|
99
99
|
self._handshake: Handshake | None = None
|
|
100
|
+
self._owns_current_process = True
|
|
100
101
|
|
|
101
102
|
@property
|
|
102
103
|
def handshake_path(self) -> Path:
|
|
@@ -108,7 +109,9 @@ class NvdaProcess:
|
|
|
108
109
|
|
|
109
110
|
@property
|
|
110
111
|
def is_running(self) -> bool:
|
|
111
|
-
|
|
112
|
+
if self._owns_current_process:
|
|
113
|
+
return self._proc is not None and self._proc.poll() is None
|
|
114
|
+
return self._handshake is not None
|
|
112
115
|
|
|
113
116
|
def _environment(self) -> dict[str, str]:
|
|
114
117
|
environment = dict(os.environ)
|
|
@@ -189,6 +192,7 @@ class NvdaProcess:
|
|
|
189
192
|
self.out_dir.mkdir(parents=True, exist_ok=True)
|
|
190
193
|
self.handshake_path.unlink(missing_ok=True)
|
|
191
194
|
self._handshake = None
|
|
195
|
+
self._owns_current_process = True
|
|
192
196
|
self._start_count += 1
|
|
193
197
|
self._number_log_file()
|
|
194
198
|
|
|
@@ -221,6 +225,46 @@ class NvdaProcess:
|
|
|
221
225
|
f"Log tail:\n{self.log_tail()}" + (f"\n\n{diagnostics}" if diagnostics else "")
|
|
222
226
|
)
|
|
223
227
|
|
|
228
|
+
def adopt_relaunched_handshake(self, *, exclude_pid: int, timeout: float = 60) -> Handshake:
|
|
229
|
+
"""Wait for NVDA's own replacement process to announce itself.
|
|
230
|
+
|
|
231
|
+
Unlike start(), this owns no subprocess handle to watch for an early
|
|
232
|
+
exit -- the process being waited for was not spawned by us, it was
|
|
233
|
+
spawned by the NVDA we are about to stop tracking via self._proc.
|
|
234
|
+
"""
|
|
235
|
+
deadline_seconds = timeout * self.timeout_scale
|
|
236
|
+
deadline = time.monotonic() + deadline_seconds
|
|
237
|
+
while time.monotonic() < deadline:
|
|
238
|
+
if self.handshake_path.is_file():
|
|
239
|
+
try:
|
|
240
|
+
payload = json.loads(self.handshake_path.read_text(encoding="utf-8"))
|
|
241
|
+
except json.JSONDecodeError:
|
|
242
|
+
time.sleep(_POLL_INTERVAL)
|
|
243
|
+
continue
|
|
244
|
+
candidate = Handshake.from_payload(payload)
|
|
245
|
+
if candidate.pid != exclude_pid:
|
|
246
|
+
self._handshake = candidate
|
|
247
|
+
self._owns_current_process = False
|
|
248
|
+
return candidate
|
|
249
|
+
time.sleep(_POLL_INTERVAL)
|
|
250
|
+
raise HandshakeTimeout(
|
|
251
|
+
f"NVDA never announced a replacement process within {deadline_seconds:.1f}s of "
|
|
252
|
+
f"restarting (still waiting on a pid other than {exclude_pid}). "
|
|
253
|
+
f"Expected {self.handshake_path}."
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
def _kill_pid(self, pid: int) -> None:
|
|
257
|
+
"""Best-effort: terminate a process we did not spawn ourselves."""
|
|
258
|
+
if sys.platform != "win32":
|
|
259
|
+
return
|
|
260
|
+
with contextlib.suppress(OSError, subprocess.TimeoutExpired):
|
|
261
|
+
subprocess.run(
|
|
262
|
+
["taskkill", "/PID", str(pid), "/F"],
|
|
263
|
+
capture_output=True,
|
|
264
|
+
check=False,
|
|
265
|
+
timeout=30,
|
|
266
|
+
)
|
|
267
|
+
|
|
224
268
|
def _request_quit(self) -> None:
|
|
225
269
|
if self._quit_via == "rpc":
|
|
226
270
|
if self._handshake is None:
|
|
@@ -242,6 +286,11 @@ class NvdaProcess:
|
|
|
242
286
|
)
|
|
243
287
|
|
|
244
288
|
def quit(self, timeout: float = 30) -> None:
|
|
289
|
+
if not self._owns_current_process:
|
|
290
|
+
if self._handshake is not None:
|
|
291
|
+
self._kill_pid(self._handshake.pid)
|
|
292
|
+
self._handshake = None
|
|
293
|
+
return
|
|
245
294
|
if self._proc is None:
|
|
246
295
|
return
|
|
247
296
|
if self._proc.poll() is not None:
|
|
@@ -259,6 +308,11 @@ class NvdaProcess:
|
|
|
259
308
|
self.kill()
|
|
260
309
|
|
|
261
310
|
def kill(self) -> None:
|
|
311
|
+
if not self._owns_current_process:
|
|
312
|
+
if self._handshake is not None:
|
|
313
|
+
self._kill_pid(self._handshake.pid)
|
|
314
|
+
self._handshake = None
|
|
315
|
+
return
|
|
262
316
|
if self._proc is None:
|
|
263
317
|
return
|
|
264
318
|
if self._proc.poll() is None:
|
|
@@ -8,13 +8,14 @@ answering this run's questions.
|
|
|
8
8
|
|
|
9
9
|
from __future__ import annotations
|
|
10
10
|
|
|
11
|
+
import http.client
|
|
11
12
|
import threading
|
|
12
13
|
import time
|
|
13
14
|
import xmlrpc.client
|
|
14
15
|
from collections.abc import Callable
|
|
15
16
|
from typing import Any
|
|
16
17
|
|
|
17
|
-
from .errors import AuthError, RpcError, WaitTimeout
|
|
18
|
+
from .errors import AuthError, ConnectionLost, RpcError, ScenarioSyntaxError, WaitTimeout
|
|
18
19
|
from .process import Handshake
|
|
19
20
|
|
|
20
21
|
_DEFAULT_INTERVAL = 0.05
|
|
@@ -71,14 +72,20 @@ class RpcClient:
|
|
|
71
72
|
"A stale NVDA from a previous run is the usual cause. "
|
|
72
73
|
f"Remote said: {message}"
|
|
73
74
|
) from fault
|
|
75
|
+
if method in ("eval_in_nvda", "exec_in_nvda") and any(
|
|
76
|
+
name in message for name in ("SyntaxError", "IndentationError", "TabError")
|
|
77
|
+
):
|
|
78
|
+
raise ScenarioSyntaxError(
|
|
79
|
+
f"{method}() was given source that doesn't compile: {message}"
|
|
80
|
+
) from fault
|
|
74
81
|
raise RpcError(f"{method}() failed inside NVDA: {message}") from fault
|
|
75
82
|
except xmlrpc.client.ProtocolError as error:
|
|
76
|
-
raise
|
|
83
|
+
raise ConnectionLost(
|
|
77
84
|
f"The spy answered {method!r} with HTTP {error.errcode} "
|
|
78
85
|
f"{error.errmsg} at {error.url}."
|
|
79
86
|
) from error
|
|
80
|
-
except OSError as error:
|
|
81
|
-
raise
|
|
87
|
+
except (OSError, http.client.HTTPException) as error:
|
|
88
|
+
raise ConnectionLost(
|
|
82
89
|
f"Could not reach the spy on 127.0.0.1:{self.port} calling {method!r}. "
|
|
83
90
|
"NVDA has probably died. "
|
|
84
91
|
f"Transport error: {error}"
|
|
@@ -230,6 +230,11 @@ class FakeSpy:
|
|
|
230
230
|
def rpc_eval_in_nvda(self, source, timeout=30.0):
|
|
231
231
|
return _marshallable(eval(source, {"__builtins__": builtins}, {}))
|
|
232
232
|
|
|
233
|
+
def rpc_exec_in_nvda(self, source, timeout=30.0):
|
|
234
|
+
scope = {"__builtins__": builtins}
|
|
235
|
+
exec(compile(source, "<fake-nvda>", "exec"), scope)
|
|
236
|
+
return _marshallable(scope.get("__result__"))
|
|
237
|
+
|
|
233
238
|
def rpc_addons_install(self, bundle_path, timeout=120.0):
|
|
234
239
|
entry = {"name": "demo-addon", "version": "1.0.0", "state": "PENDING_INSTALL"}
|
|
235
240
|
with self._lock:
|