Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Test Python Wheels #378

Merged
merged 10 commits into from
Apr 26, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 32 additions & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ jobs:
build:
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
steps:
Expand Down Expand Up @@ -62,6 +63,7 @@ jobs:
build-linux-cross:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
target: [i686-musl, armv7-musleabihf, aarch64-musl, x86_64-musl]
container:
Expand All @@ -79,11 +81,40 @@ jobs:
name: wheels
path: dist

test_wheels:
name: Test Wheels
needs: [build, build-linux-cross]
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
python-version: [2.7.17, 2.7.18, 3.5.4, 3.5.9, 3.5.10, 3.6.7, 3.6.8, 3.6.9, 3.6.10, 3.6.11, 3.6.12, 3.6.13, 3.7.1, 3.7.5, 3.7.6, 3.7.7, 3.7.8, 3.7.9, 3.7.10, 3.8.0, 3.8.1, 3.8.2, 3.8.3, 3.8.4, 3.8.5, 3.8.6, 3.8.7, 3.8.8, 3.8.9, 3.9.0, 3.9.1, 3.9.2, 3.9.3, 3.9.4]
# TODO: also test windows
os: [ubuntu-latest, macos-latest]
steps:
- uses: actions/checkout@v2
- uses: actions/download-artifact@v2
with:
name: wheels
- uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python-version }}
- name: Install wheel
run: |
pip install --no-index --find-links . py-spy
- name: Test Wheel
run: python tests/integration_test.py
if: runner.os != 'macOS'
- name: Test macOS Wheel
run: sudo "PATH=$PATH" python tests/integration_test.py
if: runner.os == 'macOS'


release:
name: Release
runs-on: ubuntu-latest
if: "startsWith(github.ref, 'refs/tags/')"
needs: [build-linux-cross, build]
needs: [test_wheels]
steps:
- uses: actions/download-artifact@v2
with:
Expand Down
43 changes: 43 additions & 0 deletions ci/update_python_test_versions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import requests
import pkg_resources
import pathlib


_VERSIONS_URL = "https://raw.githubusercontent.com/actions/python-versions/main/versions-manifest.json" # noqa


def get_github_python_versions():
versions_json = requests.get(_VERSIONS_URL).json()
raw_versions = [v["version"] for v in versions_json]
versions = []
for version_str in raw_versions:
if '-' in version_str:
continue

v = pkg_resources.parse_version(version_str)
if v.major == 3 and v.minor < 5:
# we don't support python 3.0/3.1/3.2 , and don't bother testing 3.3/3.4
continue

elif v.major == 2 and v.minor < 7:
# we don't test python support before 2.7
continue

versions.append(version_str)
return versions


if __name__ == "__main__":
versions = sorted(get_github_python_versions(), key = lambda x: pkg_resources.parse_version(x))
build_yml = pathlib.Path(__file__).parent.parent / ".github" / "workflows" / "build.yml"

transformed = []
for line in open(build_yml):
if line.startswith(" python-version: ["):
print(line)
line = f" python-version: [{', '.join(v for v in versions)}]\n"
print(line)
transformed.append(line)

with open(build_yml, "w") as o:
o.write("".join(transformed))
12 changes: 6 additions & 6 deletions src/python_bindings/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,8 @@ pub mod pyruntime {
_ => Some(1416),
}
},
Version{major: 3, minor: 8, patch: 1..=7, ..} => { Some(1416) },
Version{major: 3, minor: 9, patch: 0..=1, ..} => { Some(616) },
Version{major: 3, minor: 8, patch: 1..=9, ..} => { Some(1416) },
Version{major: 3, minor: 9, patch: 0..=4, ..} => { Some(616) },
_ => None
}
}
Expand Down Expand Up @@ -151,8 +151,8 @@ pub mod pyruntime {
_ => Some(1296)
}
},
Version{major: 3, minor: 8, patch: 1..=7, ..} => Some(1296),
Version{major: 3, minor: 9, patch: 0..=1, ..} => Some(496),
Version{major: 3, minor: 8, patch: 1..=9, ..} => Some(1296),
Version{major: 3, minor: 9, patch: 0..=4, ..} => Some(496),
_ => None
}
}
Expand All @@ -170,8 +170,8 @@ pub mod pyruntime {
_ => Some(1224)
}
},
Version{major: 3, minor: 8, patch: 1..=7, ..} => Some(1224),
Version{major: 3, minor: 9, patch: 0..=1, ..} => Some(424),
Version{major: 3, minor: 8, patch: 1..=9, ..} => Some(1224),
Version{major: 3, minor: 9, patch: 0..=4, ..} => Some(424),
_ => None
}
}
Expand Down
85 changes: 85 additions & 0 deletions tests/integration_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import json
import subprocess
import sys
import unittest
import tempfile
import os
from collections import defaultdict, namedtuple
from distutils.spawn import find_executable


Frame = namedtuple("Frame", ["file", "name", "line", "col"])

# disable gil checks on windows - just rely on active
# (doesn't seem to be working quite right - TODO: investigate)
GIL = ["--gil"] if not sys.platform.startswith("win") else []


class TestPyspy(unittest.TestCase):
""" Basic tests of using py-spy as a commandline application """
def _sample_process(self, script_name, options=None):
pyspy = find_executable("py-spy")
print("Testing py-spy @", pyspy)

# for permissions reasons, we really want to run the sampled python process as a
# subprocess of the py-spy (works best on linux etc). So we're running the
# record option, and setting different flags. To get the profile output
# we're using the speedscope format (since we can read that in as json)
with tempfile.NamedTemporaryFile() as profile_file:
cmdline = [
pyspy,
"record",
"-o",
profile_file.name,
"--format",
"speedscope",
"-d",
"1",
]
cmdline.extend(options or [])
cmdline.extend(["--", sys.executable, script_name])

subprocess.check_call(cmdline)
with open(profile_file.name) as f:
profiles = json.load(f)

frames = profiles["shared"]["frames"]
samples = defaultdict(int)
for p in profiles["profiles"]:
for sample in p["samples"]:
samples[tuple(Frame(**frames[frame]) for frame in sample)] += 1
return samples

def test_longsleep(self):
# running with the gil flag should have ~ no samples returned
profile = self._sample_process(_get_script("longsleep.py"), GIL)
assert sum(profile.values()) <= 5

# running with the idle flag should have > 95% of samples in the sleep call
profile = self._sample_process(_get_script("longsleep.py"), ["--idle"])
sample, count = _most_frequent_sample(profile)
assert count >= 95
assert len(sample) == 2
assert sample[0].name == "<module>"
assert sample[0].line == 9
assert sample[1].name == "longsleep"
assert sample[1].line == 5

def test_busyloop(self):
# can't be sure what line we're on, but we should have ~ all samples holding the gil
profile = self._sample_process(_get_script("busyloop.py"), GIL)
print(profile)
assert sum(profile.values()) >= 95



def _get_script(name):
base_dir = os.path.dirname(__file__)
return os.path.join(base_dir, "scripts", name)


def _most_frequent_sample(samples):
return max(samples.items(), key=lambda x: x[1])

if __name__ == "__main__":
unittest.main()