Skip to content

Commit b3cf751

Browse files
committed
green_mode: Choose job count based on environment
Python 3.4 and 3.5 fail under our pytest configuration when the number of jobs is the cpu count - 1. On Python 3.4, an extra cpu needs to be reserved for the tests to operate smoothly on CI, and on Python 3.5 and Windows, multiprocessing needs to be disabled entirely. Expose argument jobs throughout green_mode, so that it can be also exposed to the user or test suite for more controlled use and testing. Related to coala#295
1 parent 5027672 commit b3cf751

File tree

3 files changed

+126
-20
lines changed

3 files changed

+126
-20
lines changed

coala_quickstart/green_mode/green_mode.py

Lines changed: 66 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import itertools
33
import operator
44
import os
5+
import sys
56
from copy import deepcopy
67
from pathlib import Path
78

@@ -41,6 +42,21 @@
4142

4243

4344
settings_key = 'green_mode_infinite_value_settings'
45+
_CI_PYTEST_ACTIVE = os.environ.get('CI') and os.environ.get('PYTEST')
46+
_PYTHON_VERSION_MINOR = sys.version_info[0:2]
47+
_RESERVE_CPUS = 1
48+
49+
if _CI_PYTEST_ACTIVE: # pragma no branch; pragma Python 3.6,3.7: no cover
50+
if _PYTHON_VERSION_MINOR == (3, 5): # pragma Python 3.4: no cover
51+
# Python 3.5 runs jobs even with _RESERVE_CPUS set to 2
52+
_RESERVE_CPUS = sys.maxsize
53+
elif _PYTHON_VERSION_MINOR == (3, 4): # pragma Python 3.5: no cover
54+
_RESERVE_CPUS = 2
55+
elif os.name == 'nt': # pragma posix: no cover
56+
# FIXME: Multiprocessing not working on windows.
57+
_RESERVE_CPUS = sys.maxsize
58+
else: # pragma Python 3.4,3.5: no cover; pragma nt: no cover
59+
pass
4460

4561

4662
def initialize_project_data(dir, ignore_globs):
@@ -272,13 +288,38 @@ def check_bear_results(ret_val, ignore_ranges):
272288
return True
273289

274290

291+
def _create_mp_pool(jobs: int = 0):
292+
"""
293+
Create a multiprocessing pool.
294+
295+
:param jobs: Number of jobs to run concurrently.
296+
0 means auto-detect. 1 means no pool.
297+
"""
298+
if not isinstance(jobs, int):
299+
raise TypeError('jobs must be an int')
300+
if jobs == 1:
301+
return
302+
if jobs < 0:
303+
raise ValueError('jobs must be 0 or a positive integer')
304+
305+
import multiprocessing as mp
306+
cpu_count = mp.cpu_count()
307+
if cpu_count <= _RESERVE_CPUS:
308+
return
309+
if jobs == 0 or jobs > cpu_count - _RESERVE_CPUS:
310+
jobs = cpu_count - _RESERVE_CPUS
311+
pool = mp.Pool(processes=jobs)
312+
return pool
313+
314+
275315
def local_bear_test(bear, file_dict, file_names, lang, kwargs,
276-
ignore_ranges):
316+
ignore_ranges,
317+
jobs: int = 0,
318+
):
277319
lang_files = split_by_language(file_names)
278320
lang_files = {k.lower(): v for k, v in lang_files.items()}
279321

280-
import multiprocessing as mp
281-
pool = mp.Pool(processes=mp.cpu_count()-1)
322+
pool = _create_mp_pool(jobs)
282323

283324
file_results = []
284325

@@ -317,12 +358,11 @@ def local_bear_test(bear, file_dict, file_names, lang, kwargs,
317358
bear_obj = bear(section, None)
318359
ret_val = bear_obj.run(**dict(zip(kwargs, vals)))
319360
ret_val = [] if not ret_val else list(ret_val)
320-
# FIXME: Multiprocessing not working on windows.
321-
if os.name == 'nt': # pragma posix: no cover
322-
results.append(check_bear_results(ret_val, ignore_ranges))
323-
else: # pragma nt: no cover
361+
if pool: # pragma Python 3.5: no cover; pragma nt: no cover
324362
results.append(pool.apply(check_bear_results,
325363
args=(ret_val, ignore_ranges)))
364+
else: # pragma Python 3.4,3.6,3.7: no cover
365+
results.append(check_bear_results(ret_val, ignore_ranges))
326366

327367
for index, result in enumerate(results):
328368
if result is True:
@@ -335,14 +375,15 @@ def local_bear_test(bear, file_dict, file_names, lang, kwargs,
335375
return {bear: file_results}
336376

337377

338-
def global_bear_test(bear, file_dict, kwargs, ignore_ranges):
339-
import multiprocessing as mp
340-
pool = mp.Pool(processes=mp.cpu_count()-1)
341-
378+
def global_bear_test(bear, file_dict, kwargs, ignore_ranges,
379+
jobs: int = 0,
380+
):
342381
results = []
343382
values = []
344383
file_results = []
345384

385+
pool = _create_mp_pool(jobs)
386+
346387
for vals in itertools.product(*kwargs.values()):
347388
values.append(vals)
348389
section = Section('test-section-global-bear')
@@ -351,11 +392,11 @@ def global_bear_test(bear, file_dict, kwargs, ignore_ranges):
351392
bear_obj.file_dict = file_dict
352393
ret_val = bear_obj.run(**dict(zip(kwargs, vals)))
353394
ret_val = list(ret_val)
354-
if os.name == 'nt': # pragma posix: no cover
355-
results.append(check_bear_results(ret_val, ignore_ranges))
356-
else: # pragma nt: no cover
395+
if pool: # pragma Python 3.5: no cover; pragma nt: no cover
357396
results.append(pool.apply(check_bear_results,
358397
args=(ret_val, ignore_ranges)))
398+
else: # pragma Python 3.4,3.6,3.7: no cover
399+
results.append(check_bear_results(ret_val, ignore_ranges))
359400

360401
for index, result in enumerate(results):
361402
if result is True:
@@ -367,7 +408,9 @@ def global_bear_test(bear, file_dict, kwargs, ignore_ranges):
367408

368409

369410
def run_test_on_each_bear(bear, file_dict, file_names, lang, kwargs,
370-
ignore_ranges, type_of_setting, printer=None):
411+
ignore_ranges, type_of_setting, printer=None,
412+
jobs: int = 0,
413+
):
371414
if type_of_setting == 'non-op':
372415
printer.print('Finding suitable values to necessary '
373416
'settings for ' + bear.__name__ +
@@ -389,7 +432,8 @@ def run_test_on_each_bear(bear, file_dict, file_names, lang, kwargs,
389432

390433
def bear_test_fun(bears, bear_settings_obj, file_dict, ignore_ranges,
391434
contents, file_names, op_args_limit, value_to_op_args_limit,
392-
printer=None):
435+
printer=None,
436+
jobs: int = 0):
393437
"""
394438
Tests the bears with the generated file dict and list of files
395439
along with the values recieved for each and every type of setting
@@ -440,7 +484,9 @@ def bear_test_fun(bears, bear_settings_obj, file_dict, ignore_ranges,
440484
op_kwargs = get_kwargs(op_set, bear, contents)
441485
non_op_file_results = run_test_on_each_bear(
442486
bear, file_dict, file_names, lang, non_op_kwargs,
443-
ignore_ranges, 'non-op', printer)
487+
ignore_ranges, 'non-op', printer,
488+
jobs=jobs,
489+
)
444490
if len(op_kwargs) < op_args_limit and not(
445491
True in [len(value) > value_to_op_args_limit
446492
for key, value in op_kwargs.items()]):
@@ -449,7 +495,9 @@ def bear_test_fun(bears, bear_settings_obj, file_dict, ignore_ranges,
449495
unified_file_results = run_test_on_each_bear(
450496
bear, file_dict, file_names, lang,
451497
unified_kwargs, ignore_ranges, 'unified',
452-
printer)
498+
printer,
499+
jobs=jobs,
500+
)
453501
else:
454502
unified_file_results = None
455503
final_non_op_results.append(non_op_file_results)

setup.cfg

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@ doctest_optionflags =
2828
ELLIPSIS
2929
IGNORE_EXCEPTION_DETAIL
3030

31+
env =
32+
PYTEST=1
33+
3134
reqsfilenamepatterns =
3235
requirements.txt
3336
test-requirements.txt

tests/green_mode/green_modeTest.py

Lines changed: 57 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import operator
22
import os
3+
import sys
34
import unittest
45
import yaml
56
from copy import deepcopy
@@ -13,6 +14,7 @@
1314
from coala_quickstart.green_mode.Setting import (
1415
find_max_min_of_setting,
1516
)
17+
from coala_quickstart.green_mode import green_mode
1618
from coala_quickstart.green_mode.green_mode import (
1719
bear_test_fun,
1820
check_bear_results,
@@ -571,5 +573,58 @@ def test_write_coafile(self):
571573
self.assertIn(line, [i.strip('\\').replace('\\\\C', 'C')
572574
for i in contents.split('\n')])
573575

574-
def test_green_mode(self):
575-
pass
576+
577+
class MultiProcessingTest(unittest.TestCase):
578+
579+
def setUp(self):
580+
self.orig_cpus = green_mode._RESERVE_CPUS
581+
582+
def tearDown(self):
583+
green_mode._RESERVE_CPUS = self.orig_cpus
584+
585+
def test_no_pool(self):
586+
with self.assertRaises(TypeError):
587+
green_mode._create_mp_pool(None)
588+
with self.assertRaises(ValueError):
589+
self.assertIsNone(green_mode._create_mp_pool(-1))
590+
self.assertIsNone(green_mode._create_mp_pool(1))
591+
592+
def test_reserve_cpus(self):
593+
green_mode._RESERVE_CPUS = 1
594+
with patch('multiprocessing.cpu_count', return_value=1):
595+
self.assertIsNone(green_mode._create_mp_pool(0))
596+
green_mode._RESERVE_CPUS = 2
597+
with patch('multiprocessing.cpu_count', return_value=2):
598+
self.assertIsNone(green_mode._create_mp_pool(0))
599+
600+
green_mode._RESERVE_CPUS = 1
601+
with patch('multiprocessing.cpu_count', return_value=2):
602+
self.assertIsNotNone(green_mode._create_mp_pool(0))
603+
604+
green_mode._RESERVE_CPUS = 1
605+
with patch('multiprocessing.cpu_count', return_value=2):
606+
self.assertIsNotNone(green_mode._create_mp_pool(2))
607+
608+
green_mode._RESERVE_CPUS = 1
609+
with patch('multiprocessing.cpu_count', return_value=10):
610+
self.assertIsNotNone(green_mode._create_mp_pool(2))
611+
612+
def test_ci_pool_min(self):
613+
if not os.environ.get('CI'):
614+
return
615+
616+
if sys.version_info[0:2] == (3, 4):
617+
with patch('multiprocessing.cpu_count', return_value=2):
618+
self.assertIsNone(green_mode._create_mp_pool(0))
619+
with patch('multiprocessing.cpu_count', return_value=3):
620+
self.assertIsNotNone(green_mode._create_mp_pool(0))
621+
elif sys.version_info[0:2] == (3, 5):
622+
with patch('multiprocessing.cpu_count', return_value=3):
623+
self.assertIsNone(green_mode._create_mp_pool(0))
624+
# Python 3.5 is forced to not be mp under CI & pytest
625+
with patch('multiprocessing.cpu_count', return_value=100):
626+
self.assertIsNone(green_mode._create_mp_pool(0))
627+
else:
628+
with patch('multiprocessing.cpu_count', return_value=2):
629+
self.assertIsNotNone(green_mode._create_mp_pool(2))
630+
self.assertIsNotNone(green_mode._create_mp_pool(0))

0 commit comments

Comments
 (0)