Skip to content

Commit fda79da

Browse files
committed
[skip changelog] Integration tests are now run in parallel processes
1 parent deda040 commit fda79da

12 files changed

+254
-200
lines changed

poetry.lock

+99-19
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

+3-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ authors = []
66

77
[tool.poetry.dependencies]
88
python = "^3.8"
9-
pytest = "5.3.4"
9+
pytest = "6.0.2"
1010
simplejson = "3.17.0"
1111
semver = "2.9.0"
1212
pyserial = "3.4"
@@ -17,6 +17,8 @@ pytest-timeout = "1.3.4"
1717
invoke = "1.4.1"
1818
flake8 = "^3.8.3"
1919
black = { version = "^19.10b0", allow-prereleases = true }
20+
filelock = "^3.0.12"
21+
pytest-xdist = "^2.1.0"
2022

2123
[tool.black]
2224
line-length = 120

test/common.py

+10
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
# a commercial license, send an email to [email protected].
1515
import os
1616
import collections
17+
import json
1718

1819

1920
Board = collections.namedtuple("Board", "address fqbn package architecture id core")
@@ -25,3 +26,12 @@ def running_on_ci():
2526
"""
2627
val = os.getenv("APPVEYOR") or os.getenv("DRONE") or os.getenv("GITHUB_WORKFLOW")
2728
return val is not None
29+
30+
31+
def parse_json_traces(log_json_lines):
32+
trace_entries = []
33+
for entry in log_json_lines:
34+
entry = json.loads(entry)
35+
if entry.get("level") == "trace":
36+
trace_entries.append(entry.get("msg"))
37+
return trace_entries

test/conftest.py

+31-64
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,15 @@
1616
import platform
1717
import signal
1818
import shutil
19+
import time
1920
from pathlib import Path
2021

2122
import pytest
2223
import simplejson as json
2324
from invoke import Local
2425
from invoke.context import Context
2526
import tempfile
27+
from filelock import FileLock
2628

2729
from .common import Board
2830

@@ -54,22 +56,32 @@ def data_dir(tmpdir_factory):
5456
if platform.system() == "Windows":
5557
with tempfile.TemporaryDirectory() as tmp:
5658
yield tmp
57-
shutil.rmtree(tmp, ignore_errors=True)
59+
# shutil.rmtree(tmp, ignore_errors=True)
5860
else:
5961
data = tmpdir_factory.mktemp("ArduinoTest")
6062
yield str(data)
61-
shutil.rmtree(data, ignore_errors=True)
63+
# shutil.rmtree(data, ignore_errors=True)
6264

6365

6466
@pytest.fixture(scope="session")
65-
def downloads_dir(tmpdir_factory):
67+
def downloads_dir(tmpdir_factory, worker_id):
6668
"""
6769
To save time and bandwidth, all the tests will access
6870
the same download cache folder.
6971
"""
70-
download_dir = tmpdir_factory.mktemp("ArduinoTest")
72+
download_dir = tmpdir_factory.mktemp("ArduinoTest", numbered=False)
73+
74+
# This folders should be created only once per session, if we're running
75+
# tests in parallel using multiple processes we need to make sure this
76+
# this fixture is executed only once, thus the use of the lockfile
77+
if not worker_id == "master":
78+
lock = Path(download_dir / "lock")
79+
with FileLock(lock):
80+
if not lock.is_file():
81+
lock.touch()
82+
7183
yield str(download_dir)
72-
shutil.rmtree(download_dir, ignore_errors=True)
84+
# shutil.rmtree(download_dir, ignore_errors=True)
7385

7486

7587
@pytest.fixture(scope="function")
@@ -81,7 +93,7 @@ def working_dir(tmpdir_factory):
8193
"""
8294
work_dir = tmpdir_factory.mktemp("ArduinoTestWork")
8395
yield str(work_dir)
84-
shutil.rmtree(work_dir, ignore_errors=True)
96+
# shutil.rmtree(work_dir, ignore_errors=True)
8597

8698

8799
@pytest.fixture(scope="function")
@@ -208,61 +220,16 @@ def copy_sketch(working_dir):
208220

209221

210222
@pytest.fixture(scope="function")
211-
def core_update_index(run_command, data_dir, downloads_dir, working_dir, tmpdir_factory):
212-
"""
213-
To save time and bandwidth we install and cache cores indexes and copy them to each individual test environment
214-
"""
215-
216-
def _update_index():
217-
index_dir = tmpdir_factory.getbasetemp() / "core-indexes"
218-
if not index_dir.exists():
219-
index_dir.mkdir()
220-
env = {
221-
"ARDUINO_DATA_DIR": str(index_dir),
222-
"ARDUINO_DOWNLOADS_DIR": downloads_dir,
223-
}
224-
run_command("core update-index", working_dir, env)
225-
shutil.copytree(index_dir, data_dir, dirs_exist_ok=True)
226-
227-
return _update_index
228-
229-
230-
@pytest.fixture(scope="function")
231-
def lib_update_index(run_command, data_dir, downloads_dir, working_dir, tmpdir_factory):
232-
"""
233-
To save time and bandwidth we install and cache libraries indexes and copy them to each individual test environment
234-
"""
235-
236-
def _update_index():
237-
index_dir = tmpdir_factory.getbasetemp() / "lib-indexes"
238-
if not index_dir.exists():
239-
index_dir.mkdir()
240-
env = {
241-
"ARDUINO_DATA_DIR": str(index_dir),
242-
"ARDUINO_DOWNLOADS_DIR": downloads_dir,
243-
}
244-
run_command("lib update-index", working_dir, env)
245-
shutil.copyfile(index_dir / "library_index.json", Path(data_dir) / "library_index.json")
246-
247-
return _update_index
248-
249-
250-
@pytest.fixture(scope="function")
251-
def core_install(run_command, data_dir, downloads_dir, working_dir, tmpdir_factory):
252-
"""
253-
To save time and bandwidth we install and cache cores and copy them to each individual test environment
254-
"""
255-
data_dir = Path(data_dir) / "packages"
256-
257-
def _install(core):
258-
core_dir = tmpdir_factory.getbasetemp() / core.replace(":", "")
259-
if not core_dir.exists():
260-
core_dir.mkdir()
261-
env = {
262-
"ARDUINO_DATA_DIR": str(core_dir),
263-
"ARDUINO_DOWNLOADS_DIR": downloads_dir,
264-
}
265-
run_command(f"core install {core}", working_dir, env)
266-
shutil.copytree(core_dir / "packages", data_dir, dirs_exist_ok=True)
267-
268-
return _install
223+
def wait_for_board(run_command):
224+
def _waiter(seconds=10):
225+
# Waits for the specified amount of second for a board to be visible.
226+
# This is necessary since it might happen that a board is not immediately
227+
# available after a test upload and subsequent tests might consequently fail.
228+
time_end = time.time() + seconds
229+
while time.time() < time_end:
230+
result = run_command("board list --format json")
231+
ports = json.loads(result.stdout)
232+
if len([p.get("boards", []) for p in ports]) > 0:
233+
break
234+
235+
return _waiter

test/pytest.ini

+8-2
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,11 @@ filterwarnings =
77
markers =
88
slow: marks tests as slow (deselect with '-m "not slow"')
99

10-
# atm some tests depend on each other, better to exit at first failure (-x)
11-
addopts = -x -s --verbose --tb=short
10+
# -x to exit at first failure
11+
# -s to disable per-test capture
12+
# --verbose is what is says it is
13+
# --tb=long sets the length of the traceback in case of failures
14+
# -n=auto sets the numbers of parallel processes to use
15+
# --dist=loadfile distributes the tests in the parallel processes dividing them per file
16+
# See https://pypi.org/project/pytest-xdist/#parallelization for more info on parallelization
17+
addopts = -x -s --verbose --tb=long -n=auto --dist=loadfile

0 commit comments

Comments
 (0)