Skip to content

Commit 751ccc0

Browse files
lestevethomasjpfan
andauthored
CI Run test suite inside Pyodide (scikit-learn#27346)
Co-authored-by: Thomas J. Fan <thomasjpfan@gmail.com>
1 parent 2a548da commit 751ccc0

15 files changed

+118
-25
lines changed

azure-pipelines.yml

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -127,11 +127,11 @@ jobs:
127127
vmImage: ubuntu-22.04
128128
variables:
129129
# Need to match Python version and Emscripten version for the correct
130-
# Pyodide version. For example, for Pyodide version 0.23.4, see
131-
# https://github.com/pyodide/pyodide/blob/0.23.4/Makefile.envs
132-
PYODIDE_VERSION: '0.23.4'
133-
EMSCRIPTEN_VERSION: '3.1.32'
134-
PYTHON_VERSION: '3.11.2'
130+
# Pyodide version. For example, for Pyodide version 0.24.1, see
131+
# https://github.com/pyodide/pyodide/blob/0.24.1/Makefile.envs
132+
PYODIDE_VERSION: '0.24.1'
133+
EMSCRIPTEN_VERSION: '3.1.45'
134+
PYTHON_VERSION: '3.11.3'
135135

136136
dependsOn: [git_commit, linting]
137137
condition: |

build_tools/azure/install_pyodide.sh

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,6 @@ pyodide build
1515

1616
ls -ltrh dist
1717

18-
pyodide venv pyodide-venv
19-
source pyodide-venv/bin/activate
20-
21-
pip install dist/*.whl
22-
pip list
18+
# The Pyodide js library is needed by build_tools/azure/test_script_pyodide.sh
19+
# to run tests inside Pyodide
20+
npm install pyodide@$PYODIDE_VERSION

build_tools/azure/pytest-pyodide.js

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
const { opendir } = require('node:fs/promises');
2+
const { loadPyodide } = require("pyodide");
3+
4+
async function main() {
5+
let exit_code = 0;
6+
try {
7+
global.pyodide = await loadPyodide();
8+
let pyodide = global.pyodide;
9+
const FS = pyodide.FS;
10+
const NODEFS = FS.filesystems.NODEFS;
11+
12+
let mountDir = "/mnt";
13+
pyodide.FS.mkdir(mountDir);
14+
pyodide.FS.mount(pyodide.FS.filesystems.NODEFS, { root: "." }, mountDir);
15+
16+
await pyodide.loadPackage(["micropip"]);
17+
await pyodide.runPythonAsync(`
18+
import glob
19+
import micropip
20+
21+
wheels = glob.glob('/mnt/dist/*.whl')
22+
wheels = [f'emfs://{wheel}' for wheel in wheels]
23+
print(f'installing wheels: {wheels}')
24+
await micropip.install(wheels);
25+
26+
pkg_list = micropip.list()
27+
print(pkg_list)
28+
`);
29+
30+
// Pyodide is built without OpenMP, need to set environment variable to
31+
// skip related test
32+
await pyodide.runPythonAsync(`
33+
import os
34+
os.environ['SKLEARN_SKIP_OPENMP_TEST'] = 'true'
35+
`);
36+
37+
await pyodide.runPythonAsync("import micropip; micropip.install('pytest')");
38+
let pytest = pyodide.pyimport("pytest");
39+
let args = process.argv.slice(2);
40+
console.log('pytest args:', args);
41+
exit_code = pytest.main(pyodide.toPy(args));
42+
} catch (e) {
43+
console.error(e);
44+
// Arbitrary exit code here. I have seen this code reached instead of a
45+
// Pyodide fatal error sometimes
46+
exit_code = 66;
47+
48+
} finally {
49+
process.exit(exit_code);
50+
}
51+
}
52+
53+
main();

build_tools/azure/test_script_pyodide.sh

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,8 @@
22

33
set -e
44

5-
source pyodide-venv/bin/activate
6-
7-
pip list
8-
9-
# Need to be outside of the git clone otherwise finds non build sklearn folder
10-
cd /tmp
11-
12-
# TODO for now only testing sklearn import to make sure the wheel is not badly
13-
# broken. When Pyodide 0.24 is released we should run the full test suite and
14-
# xfail tests that fail due to Pyodide limitations
15-
python -c 'import sklearn'
5+
# We are using a pytest js wrapper script to run tests inside Pyodide. Maybe
6+
# one day we can use a Pyodide venv instead but at the time of writing
7+
# (2023-09-27) there is an issue with scipy.linalg in a Pyodide venv, see
8+
# https://github.com/pyodide/pyodide/issues/3865 for more details.
9+
node build_tools/azure/pytest-pyodide.js --pyargs sklearn --durations 20 --showlocals

sklearn/_loss/tests/test_loss.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
HuberLoss,
2828
PinballLoss,
2929
)
30-
from sklearn.utils import assert_all_finite
30+
from sklearn.utils import _IS_WASM, assert_all_finite
3131
from sklearn.utils._testing import create_memmap_backed_data, skip_if_32bit
3232

3333
ALL_LOSSES = list(_LOSSES.values())
@@ -286,6 +286,9 @@ def test_loss_dtype(
286286
287287
Also check that input arrays can be readonly, e.g. memory mapped.
288288
"""
289+
if _IS_WASM and readonly_memmap: # pragma: nocover
290+
pytest.xfail(reason="memmap not fully supported")
291+
289292
loss = loss()
290293
# generate a y_true and raw_prediction in valid range
291294
n_samples = 5

sklearn/experimental/tests/test_enable_hist_gradient_boosting.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,13 @@
22

33
import textwrap
44

5+
import pytest
6+
7+
from sklearn.utils import _IS_WASM
58
from sklearn.utils._testing import assert_run_python_script
69

710

11+
@pytest.mark.xfail(_IS_WASM, reason="cannot start subprocess")
812
def test_import_raises_warning():
913
code = """
1014
import pytest

sklearn/experimental/tests/test_enable_iterative_imputer.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,13 @@
22

33
import textwrap
44

5+
import pytest
6+
7+
from sklearn.utils import _IS_WASM
58
from sklearn.utils._testing import assert_run_python_script
69

710

11+
@pytest.mark.xfail(_IS_WASM, reason="cannot start subprocess")
812
def test_imports_strategies():
913
# Make sure different import strategies work or fail as expected.
1014

sklearn/experimental/tests/test_enable_successive_halving.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,13 @@
22

33
import textwrap
44

5+
import pytest
6+
7+
from sklearn.utils import _IS_WASM
58
from sklearn.utils._testing import assert_run_python_script
69

710

11+
@pytest.mark.xfail(_IS_WASM, reason="cannot start subprocess")
812
def test_imports_strategies():
913
# Make sure different import strategies work or fail as expected.
1014

sklearn/feature_extraction/tests/test_text.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
from sklearn.model_selection import GridSearchCV, cross_val_score, train_test_split
2727
from sklearn.pipeline import Pipeline
2828
from sklearn.svm import LinearSVC
29-
from sklearn.utils import IS_PYPY
29+
from sklearn.utils import _IS_WASM, IS_PYPY
3030
from sklearn.utils._testing import (
3131
assert_allclose_dense_sparse,
3232
assert_almost_equal,
@@ -475,6 +475,13 @@ def test_tf_idf_smoothing():
475475
assert (tfidf >= 0).all()
476476

477477

478+
@pytest.mark.xfail(
479+
_IS_WASM,
480+
reason=(
481+
"no floating point exceptions, see"
482+
" https://github.com/numpy/numpy/pull/21895#issuecomment-1311525881"
483+
),
484+
)
478485
def test_tfidf_no_smoothing():
479486
X = [[1, 1, 1], [1, 1, 0], [1, 0, 0]]
480487
tr = TfidfTransformer(smooth_idf=False, norm="l2")

sklearn/tests/test_common.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@
6060
StandardScaler,
6161
)
6262
from sklearn.semi_supervised import LabelPropagation, LabelSpreading
63-
from sklearn.utils import IS_PYPY, all_estimators
63+
from sklearn.utils import _IS_WASM, IS_PYPY, all_estimators
6464
from sklearn.utils._tags import _DEFAULT_TAGS, _safe_tags
6565
from sklearn.utils._testing import (
6666
SkipTest,
@@ -206,6 +206,7 @@ def test_class_weight_balanced_linear_classifiers(name, Classifier):
206206
check_class_weight_balanced_linear_classifier(name, Classifier)
207207

208208

209+
@pytest.mark.xfail(_IS_WASM, reason="importlib not supported for Pyodide packages")
209210
@ignore_warnings
210211
def test_import_all_consistency():
211212
# Smoke test to check that any name in a __all__ list is actually defined

0 commit comments

Comments
 (0)