Skip to content

Commit cfe09bb

Browse files
authored
Optimizing CircleCI config to reduce build times (#594)
Circle builds were taking 20-40 minutes running PyPy on one node, which really discouraged me from breaking up PRs into smaller pieces since I'd have to wait often times nearly an hour just to merge in a smaller change. This PR breaks up Circle builds into multiple different nodes. PyPy runs on three different workers using Circle's builtin `parallelism` and test-splitting features. Each of the CPython test suites run in their own node with MyPy/PyLint (to ensure we're not missing any type or linting errors across versions) and Safety (to ensure dependency versions installed on different versions do not have any known CVEs) checks. CI checks which do not vary by version are checked on Python 3.8, since it's faster than other versions.
1 parent 927d5e4 commit cfe09bb

File tree

8 files changed

+159
-38
lines changed

8 files changed

+159
-38
lines changed

.circleci/config.yml

Lines changed: 129 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,64 +1,163 @@
1-
version: 2
2-
jobs:
3-
test-cpython:
4-
docker:
5-
- image: chrisrink10/pyenv:3.6-3.7-3.8-0.0.1
6-
user: pyenv
1+
version: 2.1
2+
3+
commands:
4+
setup_tests:
5+
description: "Check out code, install Tox, and prepare the test environment."
6+
parameters:
7+
python_version:
8+
description: "Required. Python version as `major.minor`."
9+
type: string
10+
cache_key_prefix:
11+
description: "Required. Prefix used for the CircleCI cache key."
12+
type: string
713
steps:
814
- checkout
15+
- run: sudo chown -R circleci:circleci /usr/local/bin
16+
- run: sudo chown -R circleci:circleci /usr/local/lib/python<< parameters.python_version >>/site-packages
917
- restore_cache:
10-
key: deps-{{ .Branch }}-{{ checksum "tox.ini" }}-{{ checksum "Pipfile.lock" }}
18+
key: << parameters.cache_key_prefix >>1-{{ checksum "tox.ini" }}-{{ checksum "Pipfile.lock" }}
1119
- run:
1220
name: Install tox
1321
shell: /bin/bash -leo pipefail
1422
command: |
23+
pip install -U pip
1524
pip install tox
25+
26+
teardown_tests:
27+
description: "Store the cache for the current run."
28+
parameters:
29+
python_version:
30+
description: "Required. Python version as `major.minor`."
31+
type: string
32+
cache_key_prefix:
33+
description: "Required. Prefix used for the CircleCI cache key."
34+
type: string
35+
steps:
36+
- save_cache:
37+
key: << parameters.cache_key_prefix >>1-{{ checksum "tox.ini" }}-{{ checksum "Pipfile.lock" }}
38+
paths:
39+
- "/home/circleci/project/.tox"
40+
- "/usr/local/bin"
41+
- "/usr/local/lib/python<< parameters.python_version >>/site-packages"
42+
43+
run_cpython_tests:
44+
description: "Install Tox and run tests."
45+
parameters:
46+
python_version:
47+
description: "Required. Python version as `major.minor`."
48+
type: string
49+
tox_envs:
50+
description: "Required. Set of Tox environments to run on this node."
51+
type: string
52+
tox_parallel:
53+
description: "Optional. Number of parallel workers spawned by Tox."
54+
type: integer
55+
default: 2
56+
steps:
57+
- setup_tests:
58+
python_version: << parameters.python_version >>
59+
cache_key_prefix: py<< parameters.python_version >>-deps
1660
- run:
1761
name: Run Tests
1862
shell: /bin/bash -leo pipefail
1963
environment:
20-
TOX_NUM_CORES: 2
2164
TOX_PARALLEL_NO_SPINNER: 1
2265
TOX_SHOW_OUTPUT: "True"
23-
TOX_SKIP_ENV: pypy3
2466
command: |
25-
tox -p $TOX_NUM_CORES
26-
- save_cache:
27-
key: deps9-{{ .Branch }}-{{ checksum "tox.ini" }}-{{ checksum "Pipfile.lock" }}
28-
paths:
29-
- "/home/pyenv/.tox"
30-
- "/usr/local/bin"
31-
- "/usr/local/lib/python3.6/site-packages"
67+
tox -p << parameters.tox_parallel >> -e << parameters.tox_envs >>
68+
mkdir coverage
69+
mv .coverage.* "coverage/.coverage.py<< parameters.python_version >>"
70+
- teardown_tests:
71+
python_version: << parameters.python_version >>
72+
cache_key_prefix: py<< parameters.python_version >>-deps
73+
- store_artifacts:
74+
path: coverage
3275
- store_test_results:
3376
path: junit
3477

78+
jobs:
79+
test-cpython-36:
80+
docker:
81+
- image: circleci/python:3.6-buster
82+
steps:
83+
- run_cpython_tests:
84+
python_version: "3.6"
85+
tox_envs: py36,py36-mypy,py36-lint,safety
86+
87+
test-cpython-37:
88+
docker:
89+
- image: circleci/python:3.7-buster
90+
steps:
91+
- run_cpython_tests:
92+
python_version: "3.7"
93+
tox_envs: py37,py37-mypy,py37-lint,safety
94+
95+
test-cpython-38:
96+
docker:
97+
- image: circleci/python:3.8-buster
98+
steps:
99+
- run_cpython_tests:
100+
python_version: "3.8"
101+
tox_envs: py38,py38-mypy,py38-lint,format,safety
102+
103+
report-coverage:
104+
docker:
105+
- image: circleci/python:3.8-buster
106+
steps:
107+
- setup_tests:
108+
python_version: "3.8"
109+
cache_key_prefix: report-coverage-deps
110+
- run:
111+
name: Report Coverage
112+
command: |
113+
# Fetch the build numbers for this Workflow UUID
114+
RECENT_BUILDS_URL="https://circleci.com/api/v1.1/project/github/basilisp-lang/basilisp/tree/$CIRCLE_BRANCH"
115+
BUILD_NUMS=$(curl -H "Circle-Token: $CIRCLECI_API_TOKEN" "$RECENT_BUILDS_URL" | \
116+
jq -r "map(select(.workflows.workflow_id == \"$CIRCLE_WORKFLOW_ID\")) | map(.build_num) | .[]")
117+
echo "CircleCI build URL: $RECENT_BUILDS_URL"
118+
echo "CircleCI build numbers: $(echo "$BUILD_NUMS" | tr '\n' ' ')"
119+
120+
# Fetch all of the artifacts for the build numbers
121+
for build_num in $BUILD_NUMS
122+
do
123+
ARTIFACT_META_URL="https://circleci.com/api/v1.1/project/github/basilisp-lang/basilisp/$build_num/artifacts"
124+
echo "Fetching artifacts for CircleCI build from: $ARTIFACT_META_URL"
125+
ARTIFACT_URLS=$(curl -H "Circle-Token: $CIRCLECI_API_TOKEN" "$ARTIFACT_META_URL" | jq -r '.[].url')
126+
if [ -n "$ARTIFACT_URLS" ]; then
127+
echo "Found artifact URLs: $(echo "$ARTIFACT_URLS" | tr '\n' ' ')"
128+
curl -L --remote-name-all $ARTIFACT_URLS
129+
fi
130+
done
131+
tox -v -e coverage
132+
- teardown_tests:
133+
python_version: "3.8"
134+
cache_key_prefix: report-coverage-deps
135+
35136
test-pypy:
36137
docker:
37138
- image: pypy:3.6-7-slim-buster
38139
parallelism: 3
39140
steps:
40141
- checkout
41142
- restore_cache:
42-
key: deps-{{ .Branch }}-{{ checksum "tox.ini" }}-{{ checksum "Pipfile.lock" }}
143+
key: pypy-deps2-{{ checksum "tox.ini" }}-{{ checksum "Pipfile.lock" }}
43144
- run:
44145
name: Install tox
45146
command: |
147+
pip install -U pip
46148
pip install tox
47149
- run:
48150
name: Run Tests
49151
command: |
50-
# Split tests by timing and print out the test files on each node
51152
CCI_NODE_TESTS=$(circleci tests glob "tests/**/*_test.py" "tests/**/test_*.py" | circleci tests split --split-by=timings)
52153
printf "Test files:\n"
53154
echo "$CCI_NODE_TESTS"
54155
printf "\n"
55-
56-
# Run the tests on the subset defined for this node
57156
tox -e pypy3 -- $CCI_NODE_TESTS
58157
- save_cache:
59-
key: deps9-{{ .Branch }}-{{ checksum "tox.ini" }}-{{ checksum "Pipfile.lock" }}
158+
key: pypy-deps2-{{ checksum "tox.ini" }}-{{ checksum "Pipfile.lock" }}
60159
paths:
61-
- "/home/pyenv/.tox"
160+
- "/root/project/.tox"
62161
- "/usr/local/bin"
63162
- "/usr/local/lib/python3.6/site-packages"
64163
- store_test_results:
@@ -68,5 +167,12 @@ workflows:
68167
version: 2
69168
test:
70169
jobs:
71-
- test-cpython
170+
- test-cpython-36
171+
- test-cpython-37
172+
- test-cpython-38
72173
- test-pypy
174+
- report-coverage:
175+
requires:
176+
- test-cpython-36
177+
- test-cpython-37
178+
- test-cpython-38

src/basilisp/_pyast.py

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@
6464
LtE,
6565
MatMult,
6666
Mod,
67-
Module,
67+
Module as _Module,
6868
Mult,
6969
Name,
7070
NameConstant,
@@ -102,7 +102,7 @@
102102
YieldFrom,
103103
alias,
104104
arg,
105-
arguments,
105+
arguments as _arguments,
106106
boolop,
107107
cmpop,
108108
comprehension,
@@ -127,7 +127,24 @@
127127
walk,
128128
withitem,
129129
)
130-
from functools import partial
130+
131+
if sys.version_info >= (3, 8):
132+
133+
class Module(_Module):
134+
def __init__(self, *args, **kwargs):
135+
kwargs["type_ignores"] = []
136+
super().__init__(*args, **kwargs)
137+
138+
class arguments(_arguments):
139+
def __init__(self, *args, **kwargs):
140+
kwargs["posonlyargs"] = []
141+
super().__init__(*args, **kwargs)
142+
143+
144+
else:
145+
Module = _Module
146+
arguments = _arguments
147+
131148

132149
__all__ = [
133150
"AST",
@@ -257,7 +274,3 @@
257274
"walk",
258275
"withitem",
259276
]
260-
261-
if sys.version_info >= (3, 8): # pragma: no cover
262-
Module = partial(Module, type_ignores=[])
263-
arguments = partial(arguments, posonlyargs=[])

src/basilisp/lang/compiler/analyzer.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3382,7 +3382,7 @@ def _const_node(ctx: AnalyzerContext, form: ReaderForm) -> Const:
33823382

33833383

33843384
@_with_loc # noqa: MC0001
3385-
def _analyze_form( # pylint: disable=too-many-branches
3385+
def _analyze_form( # pylint: disable=too-many-branches # noqa: MC0001
33863386
ctx: AnalyzerContext, form: Union[ReaderForm, ISeq]
33873387
) -> Node:
33883388
if isinstance(form, (llist.List, ISeq)):

src/basilisp/lang/futures.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ class ProcessPoolExecutor(_ProcessPoolExecutor): # pragma: no cover
6464
def __init__(self, max_workers: Optional[int] = None):
6565
super().__init__(max_workers=max_workers)
6666

67+
# pylint: disable=arguments-differ
6768
def submit( # type: ignore
6869
self, fn: Callable[..., T], *args, **kwargs
6970
) -> "Future[T]":
@@ -78,6 +79,7 @@ def __init__(
7879
):
7980
super().__init__(max_workers=max_workers, thread_name_prefix=thread_name_prefix)
8081

82+
# pylint: disable=arguments-differ
8183
def submit( # type: ignore
8284
self, fn: Callable[..., T], *args, **kwargs
8385
) -> "Future[T]":

src/basilisp/lang/interfaces.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ def deref(self) -> Optional[T]:
2929
class IBlockingDeref(IDeref[T]):
3030
__slots__ = ()
3131

32+
# pylint: disable=arguments-differ
3233
@abstractmethod
3334
def deref(
3435
self, timeout: Optional[float] = None, timeout_val: Optional[T] = None

src/basilisp/lang/reader.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -110,9 +110,8 @@ class Comment:
110110
RawReaderForm = Union[ReaderForm, "ReaderConditional"]
111111

112112

113-
@attr.s( # pylint:disable=redefined-builtin
114-
auto_attribs=True, repr=False, slots=True, str=False
115-
)
113+
# pylint:disable=redefined-builtin
114+
@attr.s(auto_attribs=True, repr=False, slots=True, str=False)
116115
class SyntaxError(Exception):
117116
message: str
118117
line: Optional[int] = None

src/basilisp/lang/vector.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ class TransientVector(ITransientVector[T]):
2525
__slots__ = ("_inner",)
2626

2727
def __init__(self, wrapped: "PVectorEvolver[T]") -> None:
28-
self._inner = wrapped
28+
self._inner = wrapped # pylint: disable=assigning-non-slot
2929

3030
def __bool__(self):
3131
return True

tox.ini

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
[tox]
2-
envlist = py36,py37,py38,pypy3,coverage,mypy,format,lint,safety
2+
envlist = py36,py37,py38,pypy3,coverage,py{36,37,38}-mypy,py{36,37,38}-lint,format,safety
33

44
[testenv]
55
parallel_show_output = {env:TOX_SHOW_OUTPUT:true}
@@ -64,12 +64,12 @@ commands =
6464
isort --check-only
6565
black --check .
6666

67-
[testenv:mypy]
67+
[testenv:py{36,37,38}-mypy]
6868
deps = mypy
6969
commands =
7070
mypy --config-file={toxinidir}/mypy.ini --show-error-codes src/basilisp
7171

72-
[testenv:lint]
72+
[testenv:py{36,37,38}-lint]
7373
deps = prospector==1.2.0
7474
commands =
7575
prospector --profile-path={toxinidir}

0 commit comments

Comments
 (0)