Skip to content

Commit 829bf79

Browse files
authored
Run integration tests in CI (#43)
This: - adds an `integration_tests` workflow to run integration tests in CI - makes the `lint_and_test`, `integration_tests` and `check_docs` workflows reusable - makes the `release` workflow use those reusable workflows - updates the integration tests to be able to run in parallel (the building of the SDK wheel in the test fixture must run exactly once, not multiple times, or you get conflicts because of the wheel being overwritten by a different test runner) - makes all workflows use `ubuntu-latest`
1 parent 5bae238 commit 829bf79

File tree

9 files changed

+122
-68
lines changed

9 files changed

+122
-68
lines changed

.github/workflows/check_docs.yaml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
name: Check documentation status
22

3-
on: push
3+
on:
4+
workflow_call:
45

56
jobs:
67
check_docs:
78
name: Check whether documentation is up-to-date
8-
runs-on: ubuntu-20.04
9+
runs-on: ubuntu-latest
910

1011
steps:
1112
- name: Checkout repository
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
name: Integration tests
2+
3+
on:
4+
workflow_call:
5+
secrets:
6+
APIFY_TEST_USER_API_TOKEN:
7+
description: API token of the Python SDK testing user on Apify
8+
required: true
9+
10+
concurrency: # This is to make sure that only one run of this workflow is running at the same time, to not overshoot the test user limits
11+
group: integration_tests
12+
13+
jobs:
14+
integration_tests:
15+
name: Run integration tests
16+
runs-on: ubuntu-latest
17+
strategy:
18+
matrix:
19+
python-version: ["3.8", "3.9", "3.10", "3.11"]
20+
max-parallel: 1 # no concurrency on this level, to not overshoot the test user limits
21+
22+
steps:
23+
- name: Checkout repository
24+
uses: actions/checkout@v3
25+
26+
- name: Set up Python ${{ matrix.python-version }}
27+
uses: actions/setup-python@v4
28+
with:
29+
python-version: ${{ matrix.python-version }}
30+
31+
- name: Install dependencies
32+
run: make install-dev
33+
34+
- name: Run integration tests
35+
run: make INTEGRATION_TESTS_CONCURRENCY=8 integration-tests
36+
env:
37+
APIFY_TEST_USER_API_TOKEN: ${{ secrets.APIFY_TEST_USER_API_TOKEN }}

.github/workflows/lint_and_test.yaml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
name: Lint and test
22

3-
on: push
3+
on:
4+
workflow_call:
45

56
jobs:
67
lint_and_test:
78
name: Lint, check types and run unit tests
8-
runs-on: ubuntu-20.04
9+
runs-on: ubuntu-latest
910
strategy:
1011
matrix:
1112
python-version: ["3.8", "3.9", "3.10", "3.11"]

.github/workflows/release.yml

Lines changed: 11 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ on:
55
push:
66
branches:
77
- master
8+
tags-ignore:
9+
- '**'
810
# A release via GitHub releases will publish a stable version
911
release:
1012
types: [published]
@@ -23,56 +25,22 @@ on:
2325

2426
jobs:
2527
lint_and_test:
26-
name: Lint and run unit tests
27-
runs-on: ubuntu-20.04
28-
strategy:
29-
matrix:
30-
python-version: ["3.8", "3.9", "3.10", "3.11"]
28+
name: Run lint and unit tests
29+
uses: ./.github/workflows/lint_and_test.yaml
3130

32-
steps:
33-
- name: Checkout repository
34-
uses: actions/checkout@v3
35-
36-
- name: Set up Python ${{ matrix.python-version }}
37-
uses: actions/setup-python@v4
38-
with:
39-
python-version: ${{ matrix.python-version }}
40-
41-
- name: Install dependencies
42-
run: make install-dev
43-
44-
- name: Lint
45-
run: make lint
46-
47-
- name: Type check
48-
run: make type-check
49-
50-
- name: Unit tests
51-
run: make unit-tests
31+
integration_tests:
32+
name: Run integration tests
33+
uses: ./.github/workflows/integration_tests.yaml
34+
secrets: inherit
5235

5336
check_docs:
5437
name: Check whether the documentation is up to date
55-
runs-on: ubuntu-20.04
56-
57-
steps:
58-
- name: Checkout repository
59-
uses: actions/checkout@v3
60-
61-
- name: Set up Python
62-
uses: actions/setup-python@v4
63-
with:
64-
python-version: 3.8
65-
66-
- name: Install dependencies
67-
run: make install-dev
68-
69-
- name: Check whether docs are built from the latest code
70-
run: make check-docs
38+
uses: ./.github/workflows/check_docs.yaml
7139

7240
deploy:
7341
name: Publish to PyPI
74-
needs: [lint_and_test, check_docs]
75-
runs-on: ubuntu-20.04
42+
needs: [lint_and_test, integration_tests, check_docs]
43+
runs-on: ubuntu-latest
7644

7745
steps:
7846
- name: Checkout repository

.github/workflows/run_checks.yaml

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
name: Code quality checks
2+
3+
on:
4+
pull_request:
5+
6+
jobs:
7+
lint_and_test:
8+
name: Run lint and unit tests
9+
uses: ./.github/workflows/lint_and_test.yaml
10+
11+
check_docs:
12+
name: Check whether the documentation is up to date
13+
uses: ./.github/workflows/check_docs.yaml
14+
15+
integration_tests:
16+
name: Run integration tests
17+
needs: [lint_and_test, check_docs]
18+
uses: ./.github/workflows/integration_tests.yaml
19+
secrets: inherit

Makefile

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
.PHONY: clean install-dev lint test type-check check-code format docs check-docs
22

3+
# This is default for local testing, but GitHub workflows override it to a higher value in CI
4+
INTEGRATION_TESTS_CONCURRENCY = 1
5+
36
clean:
47
rm -rf build dist .mypy_cache .pytest_cache src/*.egg-info __pycache__
58

@@ -16,7 +19,7 @@ unit-tests:
1619
python3 -m pytest -n auto -ra tests/unit
1720

1821
integration-tests:
19-
python3 -m pytest -ra tests/integration
22+
python3 -m pytest -n $(INTEGRATION_TESTS_CONCURRENCY) -ra tests/integration
2023

2124
type-check:
2225
python3 -m mypy

setup.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@
6363
extras_require={
6464
'dev': [
6565
'autopep8 ~= 2.0.0',
66+
'filelock ~= 3.9.0',
6667
'flake8 ~= 5.0.4',
6768
'flake8-bugbear ~= 22.10.27',
6869
'flake8-commas ~= 2.1.0',

tests/integration/actor_source_base/Dockerfile

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
1-
# TODO: make the Python version a parameter
2-
# so we can run integration tests in GitHub with a matrix of Python versions
3-
FROM apify/actor-python:3.9
1+
# The test fixture will put the right Python version here
2+
FROM apify/actor-python:BASE_IMAGE_VERSION_PLACEHOLDER
43

54
COPY . ./
65

tests/integration/conftest.py

Lines changed: 42 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,13 @@
22
import inspect
33
import os
44
import subprocess
5+
import sys
56
import textwrap
67
from pathlib import Path
78
from typing import AsyncIterator, Awaitable, Callable, Dict, List, Mapping, Optional, Protocol, Union
89

910
import pytest
11+
from filelock import FileLock
1012

1113
from apify_client import ApifyClientAsync
1214
from apify_client.clients.resource_clients import ActorClientAsync
@@ -16,6 +18,7 @@
1618

1719
TOKEN_ENV_VAR = 'APIFY_TEST_USER_API_TOKEN'
1820
API_URL_ENV_VAR = 'APIFY_INTEGRATION_TESTS_API_URL'
21+
SDK_ROOT_PATH = Path(__file__).parent.parent.parent.resolve()
1922

2023

2124
# This fixture can't be session-scoped,
@@ -34,8 +37,40 @@ def apify_client_async() -> ApifyClientAsync:
3437
return ApifyClientAsync(api_token, api_url=api_url)
3538

3639

40+
# Build the package wheel if it hasn't been built yet, and return the path to the wheel
3741
@pytest.fixture(scope='session')
38-
def actor_base_source_files() -> Dict[str, Union[str, bytes]]:
42+
def sdk_wheel_path(tmp_path_factory: pytest.TempPathFactory, testrun_uid: str) -> Path:
43+
# Make sure the wheel is not being built concurrently across all the pytest-xdist runners,
44+
# through locking the building process with a temp file
45+
with FileLock(tmp_path_factory.getbasetemp().parent / 'sdk_wheel_build.lock'):
46+
# Make sure the wheel is built exactly once across across all the pytest-xdist runners,
47+
# through an indicator file saying that the wheel was already built
48+
was_wheel_built_this_test_run_file = tmp_path_factory.getbasetemp() / f'wheel_was_built_in_run_{testrun_uid}'
49+
if not was_wheel_built_this_test_run_file.exists():
50+
subprocess.run('python setup.py bdist_wheel', cwd=SDK_ROOT_PATH, shell=True, check=True, capture_output=True)
51+
was_wheel_built_this_test_run_file.touch()
52+
53+
# Read the current package version, necessary for getting the right wheel filename
54+
version_file = (SDK_ROOT_PATH / 'src/apify/_version.py').read_text(encoding='utf-8')
55+
sdk_version = None
56+
for line in version_file.splitlines():
57+
if line.startswith('__version__'):
58+
delim = '"' if '"' in line else "'"
59+
sdk_version = line.split(delim)[1]
60+
break
61+
else:
62+
raise RuntimeError('Unable to find version string.')
63+
64+
wheel_path = SDK_ROOT_PATH / 'dist' / f'apify-{sdk_version}-py3-none-any.whl'
65+
66+
# Just to be sure
67+
assert wheel_path.exists()
68+
69+
return wheel_path
70+
71+
72+
@pytest.fixture(scope='session')
73+
def actor_base_source_files(sdk_wheel_path: Path) -> Dict[str, Union[str, bytes]]:
3974
"""Create a dictionary of the base source files for a testing actor.
4075
4176
It takes the files from `tests/integration/actor_source_base`,
@@ -57,24 +92,14 @@ def actor_base_source_files() -> Dict[str, Union[str, bytes]]:
5792
except ValueError:
5893
source_files[relative_path] = path.read_bytes()
5994

60-
# Then build the SDK and the wheel to the source files
61-
subprocess.run('python setup.py bdist_wheel', cwd=sdk_root_path, shell=True, check=True, capture_output=True)
62-
63-
version_file = (sdk_root_path / 'src/apify/_version.py').read_text(encoding='utf-8')
64-
sdk_version = None
65-
for line in version_file.splitlines():
66-
if line.startswith('__version__'):
67-
delim = '"' if '"' in line else "'"
68-
sdk_version = line.split(delim)[1]
69-
break
70-
else:
71-
raise RuntimeError('Unable to find version string.')
95+
sdk_wheel_file_name = sdk_wheel_path.name
96+
source_files[sdk_wheel_file_name] = sdk_wheel_path.read_bytes()
7297

73-
wheel_file_name = f'apify-{sdk_version}-py3-none-any.whl'
74-
wheel_path = sdk_root_path / 'dist' / wheel_file_name
98+
source_files['requirements.txt'] = str(source_files['requirements.txt']).replace('APIFY_SDK_WHEEL_PLACEHOLDER', f'./{sdk_wheel_file_name}')
7599

76-
source_files[wheel_file_name] = wheel_path.read_bytes()
77-
source_files['requirements.txt'] = str(source_files['requirements.txt']).replace('APIFY_SDK_WHEEL_PLACEHOLDER', f'./{wheel_file_name}')
100+
current_major_minor_python_version = '.'.join([str(x) for x in sys.version_info[:2]])
101+
integration_tests_python_version = os.getenv('INTEGRATION_TESTS_PYTHON_VERSION') or current_major_minor_python_version
102+
source_files['Dockerfile'] = str(source_files['Dockerfile']).replace('BASE_IMAGE_VERSION_PLACEHOLDER', integration_tests_python_version)
78103

79104
return source_files
80105

0 commit comments

Comments
 (0)