Skip to content

[KOB-52290] Fix Python pytest generator compatibility with Appium-Python-Client 5.x#61

Open
mimosa767 wants to merge 8 commits intokobiton:masterfrom
mimosa767:feature/KOB-52290-fix-appium-options-import
Open

[KOB-52290] Fix Python pytest generator compatibility with Appium-Python-Client 5.x#61
mimosa767 wants to merge 8 commits intokobiton:masterfrom
mimosa767:feature/KOB-52290-fix-appium-options-import

Conversation

@mimosa767
Copy link
Copy Markdown
Contributor

@mimosa767 mimosa767 commented Apr 30, 2026

Here's a clean PR description you can copy in:


Title: [KOB-52290] Fix Python pytest generator compatibility with Appium-Python-Client 5.x

Description:

## Summary
Fixes two bugs in the Python pytest template that prevented generated scripts 
from running with Appium-Python-Client 5.x.

## Changes

### test_base.py
- Replace `AppiumOptions` with `UiAutomator2Options` from `appium.options.android`
- `AppiumOptions` no longer exists as a direct import in Appium-Python-Client 5.x

### proxy_server.py
- Replace `urllib.request` with `requests` library for reliable HTTPS handling
- Add 15-minute timeouts to match Java TestNG implementation
- Fix `current_command_id` — now read from `ProxyServer` instance instead of 
  stale class-level variable
- Add `force_w3c` flag — W3C error conversion only applied when JSON Wire format 
  detected on session creation
- Add device source check before adding `baseCommandId` parameter
- Use `200 <= status_code <= 299` for success detection
- Add PUT and PATCH HTTP method support
- Extract and store `kobitonSessionId` from session response

## Testing
Validated against Kobiton cloud device (Pixel 6, Android 15):
- ✅ Session created successfully
- ✅ All proxy requests forwarded with 200 OK
- ✅ Authorization headers correctly set
- ✅ kobitonSessionId extracted from session response
- ✅ baseCommandId correctly appended to Kobiton requests

mimosa767 and others added 7 commits April 15, 2026 05:39
…overage

Generated Python scripts were malformed in several ways: @classmethod landed
outside the class body, indent levels drifted with each device block causing
pytest to miss all but the first test function, finally was nested inside
except causing an immediate SyntaxError, JS true/false appeared as Python
literals, XPath selectors with double-quoted attributes broke string quoting,
and multi-device if/elif/else branches were nested inside each other.

Runtime template gaps were also fixed: missing AppiumBy/By imports in
test_app.py, deprecated webdriver.Remote API updated to AppiumOptions in
test_base.py, missing action helpers added (send_keys_to_active_element,
swipe_on_element, press_button_multiple, rotate_screen, set_location), port
handling corrected in config.py, and proxy auth switched from URL-embedded
credentials to an Authorization header in proxy_server.py.

A direct Python regression test (no gRPC) now generates a real zip from a
2-device fixture and runs ast.parse plus six structural checks over every .py
file to protect against recurrence.

Note: the pre-existing open-handles warning from the gRPC integration tests
in generate-script.test.js is unrelated and left for a separate cleanup.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The short import "from appium.options import AppiumOptions" does not exist
in Appium-Python-Client 3.1.0 and caused ImportError at pytest collection.
The class lives at appium.options.common.base, and load_capabilities is an
instance method — both corrected in test_base.py. The validator now asserts
the full import path is present so a regression is caught by the test suite.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…se shape

Live-testing a generated pytest script against api-test-green surfaced two
runtime issues in the templates:

proxy_server.py forwarded the client's Host header (localhost:<port>) to
Kobiton verbatim, so upstream routed on the wrong vhost and returned 404
for every request. It also swallowed HTTPError responses as a generic 502,
hiding the real upstream error body. Now strips hop-by-hop/routing headers
(host, content-length, connection, transfer-encoding, etc.) before
forwarding so urllib sets Host from the target URL, and propagates the
upstream status + body on HTTPError so client errors reach the Appium
driver with meaningful messages.

test_base.py find_online_device only checked the legacy deviceListData key
on /v1/devices. Newer Kobiton environments return categorized lists
(privateDevices / favoriteDevices / cloudDevices / itaTrialCloudDevices /
virtualDevices) and no deviceListData, so the helper printed "Device not
available" five times every run before falling through to session create.
Now unions all category keys.

Validator updated with matching checks so a regression on either fix is
caught by the Python integration test.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… proper auth, timeouts, W3C format conversion and kobitonSessionId extraction

- Add 15-minute timeout configuration (SOCKET_TIMEOUT_SECONDS) matching Java implementation
- Apply timeouts to all requests.get/post/delete calls
- Implement JSON Wire to W3C format conversion for /session POST responses
- Extract and store kobitonSessionId from session creation responses
- Add W3C error format conversion with Appium error code mapping
- Pass ProxyServer instance to ProxyHandler for proper state sharing
- Add detailed proxy logging for debugging request/response flow
- Add device source check — only add baseCommandId when DEVICE_SOURCE is KOBITON and current_command_id > 0
- Add force_w3c flag that's set to True only on successful /session POST with JSON Wire format detected
- Only apply W3C error conversion when force_w3c is True (prevents converting already W3C-formatted errors)
- Replace hardcoded success check with 200-299 range (200 <= status_code <= 299)
- Add do_PUT and do_PATCH HTTP methods to support PUT and PATCH requests
- Import DEVICE_SOURCES from constants for device source checking
- All changes validated with Kobiton cloud integration testing
Copilot AI review requested due to automatic review settings April 30, 2026 19:07
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR updates the Python pytest script generator/templates to run correctly with Appium-Python-Client 5.x, aligning generated code and proxy behavior with newer client/server expectations.

Changes:

  • Update generated Python templates for Appium 5.x driver initialization and add helper APIs used by generated steps.
  • Revise Python proxy forwarding to use requests, add timeouts/method support, and improve session/response handling for Kobiton.
  • Add a Python output validator + Jest integration test fixture to catch syntax/indentation/import regressions in generated output.

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
src/templates/python/test_base.py Updates driver initialization and expands helper methods used by generated tests.
src/templates/python/test_app.py Adds missing module-scope imports needed by generated locator code.
src/templates/python/proxy_server.py Reworks proxying implementation (requests-based forwarding, timeouts, session handling).
src/templates/python/config.py Fixes URL auth construction when server URL omits an explicit port.
src/services/python.js Fixes Python code generation (indentation, booleans, locator generation, action mapping).
tests/resource/python-pytest-input.json Adds a fixture input representing a 2-device pytest generation scenario.
tests/integration/python-pytest.test.js Adds an integration test that generates a zip and validates the Python output.
tests/helpers/validate-python-output.py Adds a validator script to assert generated Python syntax/structure invariants.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +56 to +62
response = requests.get(url, headers=headers, verify=False, timeout=SOCKET_TIMEOUT_SECONDS)
elif method == 'POST':
response = requests.post(url, data=body, headers=headers, verify=False, timeout=SOCKET_TIMEOUT_SECONDS)
elif method == 'DELETE':
response = requests.delete(url, headers=headers, verify=False, timeout=SOCKET_TIMEOUT_SECONDS)
else:
response = requests.request(method, url, data=body, headers=headers, verify=False, timeout=SOCKET_TIMEOUT_SECONDS)
Copy link

Copilot AI Apr 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Outbound proxy requests set verify=False, disabling TLS certificate verification. This is insecure by default and allows MITM attacks. Prefer keeping verification enabled, or make disabling verification an explicit, documented opt-in setting.

Suggested change
response = requests.get(url, headers=headers, verify=False, timeout=SOCKET_TIMEOUT_SECONDS)
elif method == 'POST':
response = requests.post(url, data=body, headers=headers, verify=False, timeout=SOCKET_TIMEOUT_SECONDS)
elif method == 'DELETE':
response = requests.delete(url, headers=headers, verify=False, timeout=SOCKET_TIMEOUT_SECONDS)
else:
response = requests.request(method, url, data=body, headers=headers, verify=False, timeout=SOCKET_TIMEOUT_SECONDS)
response = requests.get(url, headers=headers, timeout=SOCKET_TIMEOUT_SECONDS)
elif method == 'POST':
response = requests.post(url, data=body, headers=headers, timeout=SOCKET_TIMEOUT_SECONDS)
elif method == 'DELETE':
response = requests.delete(url, headers=headers, timeout=SOCKET_TIMEOUT_SECONDS)
else:
response = requests.request(method, url, data=body, headers=headers, timeout=SOCKET_TIMEOUT_SECONDS)

Copilot uses AI. Check for mistakes.
Comment on lines +18 to +20
base_url = Config.get_appium_server_url_with_auth()
print(f"[PROXY] Base URL from config: {base_url}", file=sys.stderr, flush=True)

Copy link

Copilot AI Apr 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

base_url is built from get_appium_server_url_with_auth() (which embeds username:apiKey@...) and then printed. This leaks credentials into logs. Avoid embedding secrets in URLs where possible, and never log URLs/strings that contain credentials.

Copilot uses AI. Check for mistakes.
Comment on lines +46 to +49
# Log the Authorization header
auth_header = headers.get('Authorization', 'NOT SET')
print(f"[PROXY] Authorization header: {auth_header}", file=sys.stderr, flush=True)
print(f"[PROXY] All request headers: {headers}", file=sys.stderr, flush=True)
Copy link

Copilot AI Apr 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The proxy prints the Authorization header value to stderr. This will leak credentials into CI logs and local output. Mask the header (e.g., show only scheme / last 4 chars) or remove this log entirely.

Copilot uses AI. Check for mistakes.
Comment thread src/templates/python/proxy_server.py Outdated
Comment on lines +173 to +176
self.send_response(response.status_code)
for key, val in response.headers.items():
self.send_header(key, val)
self.end_headers()
Copy link

Copilot AI Apr 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The proxy may modify response_body (session/error conversion) but still forwards upstream headers unchanged. If the upstream response contains Content-Length, it can become incorrect and cause client hangs/truncation. Drop/recompute Content-Length (and other hop-by-hop headers) based on the final body you write.

Copilot uses AI. Check for mistakes.
Comment on lines +65 to +74
# 2b. test_base.py must import AppiumOptions from its real module path.
# In Appium-Python-Client 3.x/4.x, AppiumOptions lives at
# appium.options.common.base, NOT appium.options — the shorter
# path raises ImportError at collection time.
# ------------------------------------------------------------------
test_base = py_files.get('test_base.py', '')
if 'from appium.options.common.base import AppiumOptions' not in test_base:
errors.append(
'test_base.py: missing or wrong AppiumOptions import — '
'must be "from appium.options.common.base import AppiumOptions"'
Copy link

Copilot AI Apr 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This check still requires test_base.py to import AppiumOptions from appium.options.common.base, but the template now imports UiAutomator2Options. As written, the integration test will fail even when generation is correct. Update the assertion to match the intended options import(s) (and consider covering both Android and iOS options if supported).

Suggested change
# 2b. test_base.py must import AppiumOptions from its real module path.
# In Appium-Python-Client 3.x/4.x, AppiumOptions lives at
# appium.options.common.base, NOT appium.options — the shorter
# path raises ImportError at collection time.
# ------------------------------------------------------------------
test_base = py_files.get('test_base.py', '')
if 'from appium.options.common.base import AppiumOptions' not in test_base:
errors.append(
'test_base.py: missing or wrong AppiumOptions import — '
'must be "from appium.options.common.base import AppiumOptions"'
# 2b. test_base.py must import a supported Appium options class.
# Current templates use platform-specific options such as
# UiAutomator2Options (Android) and XCUITestOptions (iOS).
# Keep AppiumOptions accepted as a legacy fallback so validation
# does not fail for otherwise-correct generated output.
# ------------------------------------------------------------------
test_base = py_files.get('test_base.py', '')
valid_options_imports = (
'from appium.options.android import UiAutomator2Options',
'from appium.options.ios import XCUITestOptions',
'from appium.options.common.base import AppiumOptions',
)
if not any(import_line in test_base for import_line in valid_options_imports):
errors.append(
'test_base.py: missing or wrong Appium options import — '
'must import one of '
'"from appium.options.android import UiAutomator2Options", '
'"from appium.options.ios import XCUITestOptions", or '
'"from appium.options.common.base import AppiumOptions"'

Copilot uses AI. Check for mistakes.
Comment on lines 52 to +55
print(f"Initialize Appium driver with desiredCaps: {desired_caps}")
server_url = self._proxy.get_server_url() + '/wd/hub'
self._driver = webdriver.Remote(server_url, desired_caps)
options = UiAutomator2Options().load_capabilities(desired_caps)
self._driver = webdriver.Remote(server_url, options=options)
Copy link

Copilot AI Apr 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

setup() always constructs UiAutomator2Options regardless of platformName. This will break iOS runs (and any non-UiAutomator2 driver) because iOS should use the iOS option class (e.g., XCUITestOptions) or a generic options builder compatible with both platforms. Consider selecting the options class based on platformName and falling back to a common options type when the platform is not Android.

Copilot uses AI. Check for mistakes.
raise last_exception

def find_visible_element_on_scrollable(self, timeout_ms, locator):
return self.find_visible_element(timeout_ms, locator)
Copy link

Copilot AI Apr 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

find_visible_element_on_scrollable() passes a single locator tuple into find_visible_element(), but find_visible_element() now iterates over locators. Passing a tuple will iterate over its two items (by/value) and then call visibility_of_element_located() with invalid locator data. Wrap the single locator in a list/tuple, or update find_visible_element() to accept both a single locator and a list of locators.

Suggested change
return self.find_visible_element(timeout_ms, locator)
return self.find_visible_element(timeout_ms, [locator])

Copilot uses AI. Check for mistakes.
…rver.py

- Convert force_w3c and kobiton_session_id to per-request state (local variables in do_request)
- Strip Content-Length header when modifying response body to prevent truncation
- Mask Authorization header in logs to prevent credential exposure
- Add threading.Lock for safe kobiton_session_id storage on server_instance
- Add get_kobiton_session_id() getter method matching Java implementation
- Add Kobiton portal URL logging in test_app.py after session creation
- Ensure proper W3C error conversion logic without cross-session contamination
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants