Skip to content

[KOB-52290][KOB-52412] Fix 5 bugs in generated Python pytest template#61

Merged
d4rkwinz merged 1 commit into
kobiton:masterfrom
mimosa767:feature/KOB-52290-fix-appium-options-import
May 13, 2026
Merged

[KOB-52290][KOB-52412] Fix 5 bugs in generated Python pytest template#61
d4rkwinz merged 1 commit into
kobiton:masterfrom
mimosa767:feature/KOB-52290-fix-appium-options-import

Conversation

@mimosa767
Copy link
Copy Markdown
Contributor

@mimosa767 mimosa767 commented Apr 30, 2026

Summary

The generated Python pytest bundles fail to run out of the box. This PR fixes all 5 template bugs identified and re-verified by @tan Tri Quach against portal v4.26.0-bd345ab.

  • test_base.py (Bug 1): import via platform-specific UiAutomator2Options (was from appium.options import AppiumOptions, which is empty in Appium-Python-Client 3.1.0)
  • test_base.py (Bug 2): call load_capabilities as an instance method (was classmethod-style call, invalid in 3.1.0)
  • test_base.py (Bug 3): device-availability check recognizes the full /v1/devices response shape (privateDevices/cloudDevices/favoriteDevices/etc.), not just legacy deviceListData
  • proxy_server.py (Bug 4): strip Host header before forwarding (was forwarding Host: localhost, which made the Kobiton gateway return 404)
  • proxy_server.py (Bug 5): rewritten on requests; forwards real upstream status/body instead of wrapping 4xx/5xx into a synthetic 502 that hid the underlying error

Also adds validator hooks in __tests__/helpers/validate-python-output.py that guard against regressions on each bug.

JIRA

  • KOB-52290 — implementation
  • KOB-52412 — QA verification (all 5 bugs reproduced against portal v4.26.0-bd345ab, all 5 verified fixed locally)

Test plan

  • Cut a manual session on portal-test-green, convert to test case, generate a Python (pytest) bundle.
  • pip3 install -r requirements.txt (Python 3.9+).
  • pytest test_suite.py -v provisions the target Kobiton device and runs the recorded steps end-to-end.
  • Repeat against both an Android and an iOS target.

Notes

  • This branch was previously open with a stale base and 8 layered commits that conflicted with master. It's now squashed to one commit on top of current master.

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.
The generated Python pytest bundles fail out of the box. Bugs identified
and re-verified by Tan Tri Quach against portal v4.26.0-bd345ab in KOB-52412:

- test_base.py: import AppiumOptions via platform-specific UiAutomator2Options
  (was: `from appium.options import AppiumOptions`, which is empty in 3.1.0)
- test_base.py: call load_capabilities as instance method
  (was: classmethod call, which is invalid in 3.1.0)
- test_base.py: device-availability check now recognizes the full
  /v1/devices response shape (privateDevices/cloudDevices/favoriteDevices
  and others), not just legacy `deviceListData`
- proxy_server.py: strip `Host` header before forwarding upstream
  (was: forwarded `Host: localhost`, which made the Kobiton gateway 404)
- proxy_server.py: rewritten on `requests`; forwards real upstream status
  and body instead of wrapping 4xx/5xx into a synthetic 502 that hid the
  underlying error

Also adds validator hooks in __tests__/helpers/validate-python-output.py
that guard against regressing each of these bugs in future generations.

Refs: KOB-52290, KOB-52412

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@mimosa767 mimosa767 force-pushed the feature/KOB-52290-fix-appium-options-import branch from d84c194 to 342945d Compare May 11, 2026 17:41
@mimosa767 mimosa767 changed the title [KOB-52290] Fix Python pytest generator compatibility with Appium-Python-Client 5.x [KOB-52290][KOB-52412] Fix 5 bugs in generated Python pytest template May 11, 2026
@mimosa767
Copy link
Copy Markdown
Contributor Author

@chuong777 @d4rkwinz — could one of you take a look at this when you get a chance? PR is rebased onto current master, squashed to one commit, all 5 bugs from KOB-52412 fixed. Tri Truong specifically suggested routing through Chuong for this one. Thanks!

@d4rkwinz
Copy link
Copy Markdown
Contributor

Sure @mimosa767
@chuong777 will take a review of it.

@d4rkwinz d4rkwinz merged commit 816a38d into kobiton:master May 13, 2026
2 checks passed
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.

3 participants