Skip to content

Commit

Permalink
Add Emscripten CI
Browse files Browse the repository at this point in the history
I xfailed most failing tests. Most fall into a few types of expected
failure:
1. Any tests that start a thread or subprocess
2. Many tests fail because Wasm doesn't handle floating point errors
correctly
3. Some tests that use mmap work correctly, others don't. I think
Emscripten's mmap syscalls mostly work but are slightly buggy. This
is an area that might be worth investigating in the future to fix
upstream.
4. Some precision tests fail, particularly for large floating point
types (128, 256). Also some of the branch cut tests. I don't really
know what is going on with these
  • Loading branch information
hoodmane committed Aug 14, 2022
1 parent 860a12e commit 10500f3
Show file tree
Hide file tree
Showing 35 changed files with 295 additions and 24 deletions.
62 changes: 62 additions & 0 deletions .github/workflows/emscripten.yml
@@ -0,0 +1,62 @@
name: Test Emscripten/Pyodide build

on:
push:
branches:
- emscripten-ci
pull_request:
branches:
- main
- maintenance/**

jobs:
build-wasm-emscripten:
# run this for now on all CI for now in case something starts breaking it
# if: "success() && (startsWith(github.ref, 'refs/tags/') || github.ref == 'refs/heads/main')"
# needs: [test, lint]
runs-on: ubuntu-latest
env:
PYODIDE_VERSION: '0.21.0'
PYTHON_VERSION: '3.10.2'
NODE_VERSION: 18
EMSCRIPTEN_VERSION: 3.1.14
steps:
- name: Checkout numpy
uses: actions/checkout@v3
with:
submodules: true
# versioneer.py requires the latest tag to be reachable. Here we
# fetch the complete history to get access to the tags.
# A shallow clone can work when the following issue is resolved:
# https://github.com/actions/checkout/issues/338
fetch-depth: 0

- name: set up python
id: setup-python
uses: actions/setup-python@v4
with:
python-version: ${{ env.PYTHON_VERSION }}

- uses: mymindstorm/setup-emsdk@v11
with:
version: ${{ env.EMSCRIPTEN_VERSION }}
actions-cache-folder: emsdk-cache

- name: Install pyodide-build
run: pip install pyodide-build==$PYODIDE_VERSION

- name: Build
run: CFLAGS=-g2 LDFLAGS=-g2 pyodide build

- name: set up node
uses: actions/setup-node@v3
with:
node-version: ${{ env.NODE_VERSION }}

- name: install Pyodide
run: |
cd emscripten
npm i pyodide@$PYODIDE_VERSION
- name: Test
run: |
node emscripten/emscripten_runner.js . ./numpy
118 changes: 118 additions & 0 deletions emscripten/emscripten_runner.js
@@ -0,0 +1,118 @@
const { opendir } = require('node:fs/promises');
const { loadPyodide } = require('pyodide');

async function findWheel(distDir) {
const dir = await opendir(distDir);
for await (const dirent of dir) {
if (dirent.name.endsWith('wasm32.whl')) {
return dirent.name;
}
}
}

const pkgDir = process.argv[2];
const distDir = pkgDir + '/dist';

function make_tty_ops(stream){
return {
// get_char has 3 particular return values:
// a.) the next character represented as an integer
// b.) undefined to signal that no data is currently available
// c.) null to signal an EOF
get_char(tty) {
if (!tty.input.length) {
var result = null;
var BUFSIZE = 256;
var buf = Buffer.alloc(BUFSIZE);
var bytesRead = fs.readSync(process.stdin.fd, buf, 0, BUFSIZE, -1);
if (bytesRead === 0) {
return null;
}
result = buf.slice(0, bytesRead);
tty.input = Array.from(result);
}
return tty.input.shift();
},
put_char(tty, val) {
try {
if(val !== null){
tty.output.push(val);
}
if (val === null || val === 10) {
process.stdout.write(Buffer.from(tty.output));
tty.output = [];
}
} catch(e){
console.warn(e);
}
},
flush(tty) {
if (!tty.output || tty.output.length === 0) {
return;
}
stream.write(Buffer.from(tty.output));
tty.output = [];
}
};
}

function setupStreams(FS, TTY){
let mytty = FS.makedev(FS.createDevice.major++, 0);
let myttyerr = FS.makedev(FS.createDevice.major++, 0);
TTY.register(mytty, make_tty_ops(process.stdout))
TTY.register(myttyerr, make_tty_ops(process.stderr))
FS.mkdev('/dev/mytty', mytty);
FS.mkdev('/dev/myttyerr', myttyerr);
FS.unlink('/dev/stdin');
FS.unlink('/dev/stdout');
FS.unlink('/dev/stderr');
FS.symlink('/dev/mytty', '/dev/stdin');
FS.symlink('/dev/mytty', '/dev/stdout');
FS.symlink('/dev/myttyerr', '/dev/stderr');
FS.closeStream(0);
FS.closeStream(1);
FS.closeStream(2);
FS.open('/dev/stdin', 0);
FS.open('/dev/stdout', 1);
FS.open('/dev/stderr', 1);
}

async function main() {
const wheelName = await findWheel(distDir);
const wheelURL = `file:${distDir}/${wheelName}`;
let exitcode = 0;
try {
pyodide = await loadPyodide();
const FS = pyodide.FS;
setupStreams(FS, pyodide._module.TTY);
const NODEFS = FS.filesystems.NODEFS;
FS.chdir("/lib/python3.10/site-packages/");

await pyodide.loadPackage(['micropip']);
await pyodide.runPythonAsync(`
from pathlib import Path
import micropip
# reqs = [x for x in Path("./test_requirements.txt").read_text().split() if not x.startswith("#")]
reqs = ["cython>=0.29.30,<3.0",
"wheel==0.37.0",
"setuptools==59.2.0",
"hypothesis==6.24.1",
"pytest==6.2.5",
"pytz==2021.3",
"typing_extensions>=4.2.0",]
reqs.extend(["tomli", "${wheelURL}"])
await micropip.install(reqs)
`);
const pytest = pyodide.pyimport('pytest');
exitcode = pytest.main(pyodide.toPy([...process.argv.slice(3)]));
} catch (e) {
console.error(e);
exitcode = 1;
} finally {
// worker.terminate();
process.exit(exitcode);
}
}

main();
3 changes: 2 additions & 1 deletion numpy/core/tests/test_casting_floatingpoint_errors.py
@@ -1,6 +1,6 @@
import pytest
from pytest import param

from numpy.testing import IS_WASM
import numpy as np


Expand Down Expand Up @@ -136,6 +136,7 @@ def flat_assignment():

yield flat_assignment

@pytest.mark.skipif(IS_WASM, reason="Wasm doesn't support floating point errors")
@pytest.mark.parametrize(["value", "dtype"], values_and_dtypes())
@pytest.mark.filterwarnings("ignore::numpy.ComplexWarning")
def test_floatingpoint_errors_casting(dtype, value):
Expand Down
3 changes: 3 additions & 0 deletions numpy/core/tests/test_cython.py
Expand Up @@ -5,6 +5,7 @@
import pytest

import numpy as np
from numpy.testing import IS_WASM

# This import is copied from random.tests.test_extending
try:
Expand All @@ -30,6 +31,8 @@
@pytest.fixture
def install_temp(request, tmp_path):
# Based in part on test_cython from random.tests.test_extending
if IS_WASM:
pytest.skip("No subprocess")

here = os.path.dirname(__file__)
ext_dir = os.path.join(here, "examples", "cython")
Expand Down
4 changes: 4 additions & 0 deletions numpy/core/tests/test_datetime.py
Expand Up @@ -4,6 +4,7 @@
import datetime
import pytest
from numpy.testing import (
IS_WASM,
assert_, assert_equal, assert_raises, assert_warns, suppress_warnings,
assert_raises_regex, assert_array_equal,
)
Expand Down Expand Up @@ -1294,6 +1295,7 @@ def check(a, b, res):
def test_timedelta_floor_divide(self, op1, op2, exp):
assert_equal(op1 // op2, exp)

@pytest.mark.skipif(IS_WASM, reason="fp errors don't work in wasm")
@pytest.mark.parametrize("op1, op2", [
# div by 0
(np.timedelta64(10, 'us'),
Expand Down Expand Up @@ -1368,6 +1370,7 @@ def test_timedelta_divmod(self, op1, op2):
expected = (op1 // op2, op1 % op2)
assert_equal(divmod(op1, op2), expected)

@pytest.mark.skipif(IS_WASM, reason="does not work in wasm")
@pytest.mark.parametrize("op1, op2", [
# reuse cases from floordiv
# div by 0
Expand Down Expand Up @@ -1993,6 +1996,7 @@ def test_timedelta_modulus_error(self, val1, val2):
with assert_raises_regex(TypeError, "common metadata divisor"):
val1 % val2

@pytest.mark.skipif(IS_WASM, reason="fp errors don't work in wasm")
def test_timedelta_modulus_div_by_zero(self):
with assert_warns(RuntimeWarning):
actual = np.timedelta64(10, 's') % np.timedelta64(0, 's')
Expand Down
4 changes: 3 additions & 1 deletion numpy/core/tests/test_errstate.py
Expand Up @@ -2,7 +2,7 @@
import sysconfig

import numpy as np
from numpy.testing import assert_, assert_raises
from numpy.testing import assert_, assert_raises, IS_WASM

# The floating point emulation on ARM EABI systems lacking a hardware FPU is
# known to be buggy. This is an attempt to identify these hosts. It may not
Expand All @@ -12,6 +12,7 @@
arm_softfloat = False if hosttype is None else hosttype.endswith('gnueabi')

class TestErrstate:
@pytest.mark.skipif(IS_WASM, reason="fp errors don't work in wasm")
@pytest.mark.skipif(arm_softfloat,
reason='platform/cpu issue with FPU (gh-413,-15562)')
def test_invalid(self):
Expand All @@ -24,6 +25,7 @@ def test_invalid(self):
with assert_raises(FloatingPointError):
np.sqrt(a)

@pytest.mark.skipif(IS_WASM, reason="fp errors don't work in wasm")
@pytest.mark.skipif(arm_softfloat,
reason='platform/cpu issue with FPU (gh-15562)')
def test_divide(self):
Expand Down
4 changes: 3 additions & 1 deletion numpy/core/tests/test_half.py
Expand Up @@ -3,7 +3,7 @@

import numpy as np
from numpy import uint16, float16, float32, float64
from numpy.testing import assert_, assert_equal, _OLD_PROMOTION
from numpy.testing import assert_, assert_equal, _OLD_PROMOTION, IS_WASM


def assert_raises_fpe(strmatch, callable, *args, **kwargs):
Expand Down Expand Up @@ -483,6 +483,8 @@ def test_half_coercion(self, weak_promotion):

@pytest.mark.skipif(platform.machine() == "armv5tel",
reason="See gh-413.")
@pytest.mark.skipif(IS_WASM,
reason="fp exceptions don't work in wasm.")
def test_half_fpe(self):
with np.errstate(all='raise'):
sx16 = np.array((1e-4,), dtype=float16)
Expand Down
3 changes: 2 additions & 1 deletion numpy/core/tests/test_indexing.py
Expand Up @@ -10,7 +10,7 @@
from itertools import product
from numpy.testing import (
assert_, assert_equal, assert_raises, assert_raises_regex,
assert_array_equal, assert_warns, HAS_REFCOUNT,
assert_array_equal, assert_warns, HAS_REFCOUNT, IS_WASM
)


Expand Down Expand Up @@ -563,6 +563,7 @@ def test_too_many_advanced_indices(self, index, num, original_ndim):
with pytest.raises(IndexError):
arr[(index,) * num] = 1.

@pytest.mark.skipif(IS_WASM, reason="no threading")
def test_structured_advanced_indexing(self):
# Test that copyswap(n) used by integer array indexing is threadsafe
# for structured datatypes, see gh-15387. This test can behave randomly.
Expand Down
3 changes: 3 additions & 0 deletions numpy/core/tests/test_limited_api.py
Expand Up @@ -5,7 +5,10 @@
import sysconfig
import pytest

from numpy.testing import IS_WASM


@pytest.mark.skipif(IS_WASM, reason="Can't start subprocess")
@pytest.mark.xfail(
sysconfig.get_config_var("Py_DEBUG"),
reason=(
Expand Down
4 changes: 3 additions & 1 deletion numpy/core/tests/test_mem_policy.py
Expand Up @@ -5,7 +5,7 @@
import numpy as np
import threading
import warnings
from numpy.testing import extbuild, assert_warns
from numpy.testing import extbuild, assert_warns, IS_WASM
import sys


Expand All @@ -18,6 +18,8 @@ def get_module(tmp_path):
"""
if sys.platform.startswith('cygwin'):
pytest.skip('link fails on cygwin')
if IS_WASM:
pytest.skip("Can't build module inside Wasm")
functions = [
("get_default_policy", "METH_NOARGS", """
Py_INCREF(PyDataMem_DefaultHandler);
Expand Down
3 changes: 2 additions & 1 deletion numpy/core/tests/test_nditer.py
Expand Up @@ -9,7 +9,7 @@
from numpy import array, arange, nditer, all
from numpy.testing import (
assert_, assert_equal, assert_array_equal, assert_raises,
HAS_REFCOUNT, suppress_warnings, break_cycles
IS_WASM, HAS_REFCOUNT, suppress_warnings, break_cycles
)


Expand Down Expand Up @@ -2015,6 +2015,7 @@ def test_buffered_cast_error_paths():
buf = next(it)
buf[...] = "a" # cannot be converted to int.

@pytest.mark.skipif(IS_WASM, reason="Cannot start subprocess")
@pytest.mark.skipif(not HAS_REFCOUNT, reason="PyPy seems to not hit this.")
def test_buffered_cast_error_paths_unraisable():
# The following gives an unraisable error. Pytest sometimes captures that
Expand Down
2 changes: 2 additions & 0 deletions numpy/core/tests/test_nep50_promotions.py
Expand Up @@ -6,6 +6,7 @@

import numpy as np
import pytest
from numpy.testing import IS_WASM


@pytest.fixture(scope="module", autouse=True)
Expand All @@ -16,6 +17,7 @@ def _weak_promotion_enabled():
np._set_promotion_state(state)


@pytest.mark.skipif(IS_WASM, reason="wasm doesn't have support for fp errors")
def test_nep50_examples():
with pytest.warns(UserWarning, match="result dtype changed"):
res = np.uint8(1) + 2
Expand Down

0 comments on commit 10500f3

Please sign in to comment.