mirror of
https://github.com/apache/arrow.git
synced 2026-10-28 16:47:19 +08:00
### Rationale for this change Installing the GCS testbench using the same Python that's being used to test PyArrow is fragile: some testbench versions may not be compatible, or there could be conflicts among the dependencies of the respective libraries. ### What changes are included in this PR? Use `pipx` to install the GCS testbench in a separate, controlled environment, using an appropriate Python version. ### Are these changes tested? Yes, by CI. ### Are there any user-facing changes? No. * GitHub Issue: #38183 Authored-by: Antoine Pitrou <antoine@python.org> Signed-off-by: Sutou Kouhei <kou@clear-code.com>
344 lines
11 KiB
Python
344 lines
11 KiB
Python
# Licensed to the Apache Software Foundation (ASF) under one
|
|
# or more contributor license agreements. See the NOTICE file
|
|
# distributed with this work for additional information
|
|
# regarding copyright ownership. The ASF licenses this file
|
|
# to you under the Apache License, Version 2.0 (the
|
|
# "License"); you may not use this file except in compliance
|
|
# with the License. You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing,
|
|
# software distributed under the License is distributed on an
|
|
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
# KIND, either express or implied. See the License for the
|
|
# specific language governing permissions and limitations
|
|
# under the License.
|
|
|
|
|
|
import argparse
|
|
import contextlib
|
|
import http.server
|
|
import os
|
|
import queue
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
import time
|
|
import threading
|
|
|
|
from pathlib import Path
|
|
from io import BytesIO
|
|
|
|
from selenium import webdriver
|
|
|
|
|
|
class TemplateOverrider(http.server.SimpleHTTPRequestHandler):
|
|
def log_request(self, code="-", size="-"):
|
|
# don't log successful requests
|
|
return
|
|
|
|
def do_GET(self) -> bytes | None:
|
|
if self.path.endswith(PYARROW_WHEEL_PATH.name):
|
|
self.send_response(200)
|
|
self.send_header("Content-type", "application/x-zip")
|
|
self.end_headers()
|
|
with PYARROW_WHEEL_PATH.open(mode="rb") as wheel:
|
|
self.copyfile(wheel, self.wfile)
|
|
if self.path.endswith("/test.html"):
|
|
body = b"""
|
|
<!doctype html>
|
|
<html>
|
|
<head>
|
|
<script>
|
|
window.python_done_callback = undefined;
|
|
window.python_logs = [];
|
|
function capturelogs(evt) {
|
|
if ('results' in evt.data) {
|
|
if (window.python_done_callback) {
|
|
let callback = window.python_done_callback;
|
|
window.python_done_callback = undefined;
|
|
callback({result:evt.data.results});
|
|
}
|
|
}
|
|
if ('print' in evt.data) {
|
|
evt.data.print.forEach((x)=>{window.python_logs.push(x)});
|
|
}
|
|
}
|
|
window.pyworker = new Worker("worker.js");
|
|
window.pyworker.onmessage = capturelogs;
|
|
</script>
|
|
</head>
|
|
<body></body>
|
|
</html>
|
|
"""
|
|
self.send_response(200)
|
|
self.send_header("Content-type", "text/html")
|
|
self.send_header("Content-length", len(body))
|
|
self.end_headers()
|
|
self.copyfile(BytesIO(body), self.wfile)
|
|
elif self.path.endswith("/worker.js"):
|
|
body = b"""
|
|
importScripts("./pyodide.js");
|
|
onmessage = async function (e) {
|
|
const data = e.data;
|
|
if (!self.pyodide) {
|
|
self.pyodide = await loadPyodide();
|
|
}
|
|
function do_print(arg) {
|
|
let databytes = Array.from(arg);
|
|
self.postMessage({print:databytes});
|
|
return databytes.length;
|
|
}
|
|
self.pyodide.setStdout({write:do_print,isatty:data.isatty});
|
|
self.pyodide.setStderr({write:do_print,isatty:data.isatty});
|
|
|
|
await self.pyodide.loadPackagesFromImports(data.python);
|
|
let results = await self.pyodide.runPythonAsync(data.python);
|
|
self.postMessage({results});
|
|
}
|
|
"""
|
|
self.send_response(200)
|
|
self.send_header("Content-type", "application/javascript")
|
|
self.send_header("Content-length", len(body))
|
|
self.end_headers()
|
|
self.copyfile(BytesIO(body), self.wfile)
|
|
|
|
else:
|
|
return super().do_GET()
|
|
|
|
def end_headers(self):
|
|
# Enable Cross-Origin Resource Sharing (CORS)
|
|
self.send_header("Access-Control-Allow-Origin", "*")
|
|
super().end_headers()
|
|
|
|
|
|
def run_server_thread(dist_dir, q):
|
|
global _SERVER_ADDRESS
|
|
os.chdir(dist_dir)
|
|
server = http.server.HTTPServer(("", 0), TemplateOverrider)
|
|
q.put(server.server_address)
|
|
print(f"Starting server for {dist_dir} at: {server.server_address}")
|
|
server.serve_forever()
|
|
|
|
|
|
@contextlib.contextmanager
|
|
def launch_server(dist_dir):
|
|
q = queue.Queue()
|
|
p = threading.Thread(target=run_server_thread, args=[dist_dir, q], daemon=True)
|
|
p.start()
|
|
address = q.get(timeout=50)
|
|
time.sleep(0.1) # wait to make sure server is started
|
|
yield address
|
|
p.terminate()
|
|
|
|
|
|
class NodeDriver:
|
|
import subprocess
|
|
|
|
def __init__(self, hostname, port):
|
|
self.process = subprocess.Popen(
|
|
[shutil.which("script"), "-c", shutil.which("node")],
|
|
stdin=subprocess.PIPE,
|
|
shell=False,
|
|
bufsize=0,
|
|
)
|
|
print(self.process)
|
|
time.sleep(0.1) # wait for node to start
|
|
self.hostname = hostname
|
|
self.port = port
|
|
self.last_ret_code = None
|
|
|
|
def load_pyodide(self, dist_dir):
|
|
self.execute_js(
|
|
f"""
|
|
const {{ loadPyodide }} = require('{dist_dir}/pyodide.js');
|
|
let pyodide = await loadPyodide();
|
|
"""
|
|
)
|
|
|
|
def clear_logs(self):
|
|
pass # we don't handle logs for node
|
|
|
|
def write_stdin(self, buffer):
|
|
# because we use unbuffered IO for
|
|
# stdout, stdin.write is also unbuffered
|
|
# so might under-run on writes
|
|
while len(buffer) > 0 and self.process.poll() is None:
|
|
written = self.process.stdin.write(buffer)
|
|
if written == len(buffer):
|
|
break
|
|
elif written == 0:
|
|
# full buffer - wait
|
|
time.sleep(0.01)
|
|
else:
|
|
buffer = buffer[written:]
|
|
|
|
def execute_js(self, code, wait_for_terminate=True):
|
|
self.write_stdin((code + "\n").encode("utf-8"))
|
|
|
|
def load_arrow(self):
|
|
self.execute_js(f"await pyodide.loadPackage('{PYARROW_WHEEL_PATH}');")
|
|
|
|
def execute_python(self, code, wait_for_terminate=True):
|
|
js_code = f"""
|
|
python = `{code}`;
|
|
await pyodide.loadPackagesFromImports(python);
|
|
python_output = await pyodide.runPythonAsync(python);
|
|
"""
|
|
self.last_ret_code = self.execute_js(js_code, wait_for_terminate)
|
|
return self.last_ret_code
|
|
|
|
def wait_for_done(self):
|
|
# in node we just let it run above
|
|
# then send EOF and join process
|
|
self.write_stdin(b"process.exit(python_output)\n")
|
|
return self.process.wait()
|
|
|
|
|
|
class BrowserDriver:
|
|
def __init__(self, hostname, port, driver):
|
|
self.driver = driver
|
|
self.driver.get(f"http://{hostname}:{port}/test.html")
|
|
self.driver.set_script_timeout(100)
|
|
|
|
def load_pyodide(self, dist_dir):
|
|
pass
|
|
|
|
def load_arrow(self):
|
|
self.execute_python(
|
|
f"import pyodide_js as pjs\n"
|
|
f"await pjs.loadPackage('{PYARROW_WHEEL_PATH.name}')\n"
|
|
)
|
|
|
|
def execute_python(self, code, wait_for_terminate=True):
|
|
if wait_for_terminate:
|
|
self.driver.execute_async_script(
|
|
f"""
|
|
let callback = arguments[arguments.length-1];
|
|
python = `{code}`;
|
|
window.python_done_callback = callback;
|
|
window.pyworker.postMessage(
|
|
{{python, isatty: {'true' if sys.stdout.isatty() else 'false'}}});
|
|
"""
|
|
)
|
|
else:
|
|
self.driver.execute_script(
|
|
f"""
|
|
let python = `{code}`;
|
|
window.python_done_callback= (x) => {{window.python_script_done=x;}};
|
|
window.pyworker.postMessage(
|
|
{{python,isatty:{'true' if sys.stdout.isatty() else 'false'}}});
|
|
"""
|
|
)
|
|
|
|
def clear_logs(self):
|
|
self.driver.execute_script("window.python_logs = [];")
|
|
|
|
def wait_for_done(self):
|
|
while True:
|
|
# poll for console.log messages from our webworker
|
|
# which are the output of pytest
|
|
lines = self.driver.execute_script(
|
|
"let temp = window.python_logs;window.python_logs=[];return temp;"
|
|
)
|
|
if len(lines) > 0:
|
|
sys.stdout.buffer.write(bytes(lines))
|
|
done = self.driver.execute_script("return window.python_script_done;")
|
|
if done is not None:
|
|
value = done["result"]
|
|
self.driver.execute_script("delete window.python_script_done;")
|
|
return value
|
|
time.sleep(0.1)
|
|
|
|
|
|
class ChromeDriver(BrowserDriver):
|
|
def __init__(self, hostname, port):
|
|
from selenium.webdriver.chrome.options import Options
|
|
|
|
options = Options()
|
|
options.add_argument("--headless")
|
|
options.add_argument("--no-sandbox")
|
|
super().__init__(hostname, port, webdriver.Chrome(options=options))
|
|
|
|
|
|
class FirefoxDriver(BrowserDriver):
|
|
def __init__(self, hostname, port):
|
|
from selenium.webdriver.firefox.options import Options
|
|
|
|
options = Options()
|
|
options.add_argument("--headless")
|
|
|
|
super().__init__(hostname, port, webdriver.Firefox(options=options))
|
|
|
|
|
|
def _load_pyarrow_in_runner(driver, wheel_name):
|
|
driver.load_arrow()
|
|
driver.execute_python(
|
|
"""import sys
|
|
import micropip
|
|
if "pyarrow" not in sys.modules:
|
|
await micropip.install("hypothesis")
|
|
import pyodide_js as pjs
|
|
await pjs.loadPackage("numpy")
|
|
await pjs.loadPackage("pandas")
|
|
import pytest
|
|
import pandas # import pandas after pyarrow package load for pandas/pyarrow
|
|
# functions to work
|
|
import pyarrow
|
|
""",
|
|
wait_for_terminate=True,
|
|
)
|
|
|
|
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument(
|
|
"-d",
|
|
"--dist-dir",
|
|
type=str,
|
|
help="Pyodide distribution directory",
|
|
default="./pyodide",
|
|
)
|
|
parser.add_argument("wheel", type=str, help="Wheel to run tests from")
|
|
parser.add_argument(
|
|
"-t", "--test-submodule", help="Submodule that tests live in", default="test"
|
|
)
|
|
parser.add_argument(
|
|
"-r",
|
|
"--runtime",
|
|
type=str,
|
|
choices=["chrome", "node", "firefox"],
|
|
help="Runtime to run tests in",
|
|
default="chrome",
|
|
)
|
|
args = parser.parse_args()
|
|
|
|
PYARROW_WHEEL_PATH = Path(args.wheel).resolve()
|
|
|
|
dist_dir = Path(os.getcwd(), args.dist_dir).resolve()
|
|
print(f"dist dir={dist_dir}")
|
|
with launch_server(dist_dir) as (hostname, port):
|
|
if args.runtime == "chrome":
|
|
driver = ChromeDriver(hostname, port)
|
|
elif args.runtime == "node":
|
|
driver = NodeDriver(hostname, port)
|
|
elif args.runtime == "firefox":
|
|
driver = FirefoxDriver(hostname, port)
|
|
|
|
print("Load pyodide in browser")
|
|
driver.load_pyodide(dist_dir)
|
|
print("Load pyarrow in browser")
|
|
_load_pyarrow_in_runner(driver, Path(args.wheel).name)
|
|
driver.clear_logs()
|
|
print("Run pytest in browser")
|
|
driver.execute_python(
|
|
"""
|
|
import pyarrow,pathlib
|
|
pyarrow_dir = pathlib.Path(pyarrow.__file__).parent
|
|
pytest.main([pyarrow_dir, '-r', 's'])
|
|
""",
|
|
wait_for_terminate=False,
|
|
)
|
|
print("Wait for done")
|
|
os._exit(driver.wait_for_done())
|