Skip to content

Commit f150802

Browse files
detect whether kernel.perf_event_paranoid is set (#97)
* Check if perf counters are enabled * Add tests * Fix tests on macOS * Fix checking function * Change workflow * Fix perf counters enabled check * Add comment describing why temp file is used for communicating with the process * Change error message * Add information that oiejq doesn't work outside of Linux * Change fail message when using `oiejq` outside of linux * Change the fail message for oiejq on macOS and Windows * Apply suggestion from code review Co-authored-by: Tomasz Nowak <[email protected]> * Change fail message for disabled perf counters * Apply suggestion from code review Co-authored-by: Tomasz Nowak <[email protected]> * Update src/sinol_make/commands/run/__init__.py Co-authored-by: Tomasz Nowak <[email protected]> --------- Co-authored-by: Tomasz Nowak <[email protected]>
1 parent 9910da3 commit f150802

File tree

10 files changed

+188
-112
lines changed

10 files changed

+188
-112
lines changed

setup.cfg

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,3 +47,4 @@ testpaths =
4747
tests
4848
markers =
4949
github_runner: Mark tests that require GitHub runner
50+
oiejq: Mark tests that require working oiejq

src/sinol_make/__init__.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import sys
77
import os
88

9-
from sinol_make import util
9+
from sinol_make import util, oiejq
1010

1111
__version__ = "1.5.0"
1212

@@ -45,11 +45,11 @@ def main_exn():
4545
f'New version of sinol-make is available (your version: {__version__}, available version: {new_version}).\n'
4646
f' You can update it by running `pip3 install sinol-make --upgrade`.'))
4747

48-
if sys.platform == 'linux' and not util.check_oiejq():
48+
if sys.platform == 'linux' and not oiejq.check_oiejq():
4949
print(util.warning('`oiejq` in `~/.local/bin/` not detected, installing now...'))
5050

5151
try:
52-
if util.install_oiejq():
52+
if oiejq.install_oiejq():
5353
print(util.info('`oiejq` was successfully installed.'))
5454
else:
5555
util.exit_with_error('`oiejq` could not be installed.\n'

src/sinol_make/commands/run/__init__.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import glob
99
from typing import Dict
1010

11+
from sinol_make import oiejq
1112
from sinol_make.commands.run.structs import ExecutionResult, ResultChange, ValidationResult, ExecutionData, \
1213
PointsChange, PrintData
1314
from sinol_make.helpers.parsers import add_compilation_arguments
@@ -917,13 +918,20 @@ def validate_arguments(self, args):
917918
timetool_path = None
918919
if args.time_tool == 'oiejq':
919920
if sys.platform != 'linux':
920-
util.exit_with_error('oiejq is only available on Linux.')
921+
util.exit_with_error('As `oiejq` works only on Linux-based operating systems,\n'
922+
'we do not recommend using operating systems such as Windows or macOS.\n'
923+
'Nevertheless, you can still run sinol-make by specifying\n'
924+
'another way of measuring time through the `--time-tool` flag.\n'
925+
'See `sinol-make run --help` for more information about the flag.\n'
926+
'See https://github.com/sio2project/sinol-make#why for more information about `oiejq`.\n')
927+
928+
oiejq.check_perf_counters_enabled()
921929
if 'oiejq_path' in args and args.oiejq_path is not None:
922-
if not util.check_oiejq(args.oiejq_path):
930+
if not oiejq.check_oiejq(args.oiejq_path):
923931
util.exit_with_error('Invalid oiejq path.')
924932
timetool_path = args.oiejq_path
925933
else:
926-
timetool_path = util.get_oiejq_path()
934+
timetool_path = oiejq.get_oiejq_path()
927935
if timetool_path is None:
928936
util.exit_with_error('oiejq is not installed.')
929937
elif args.time_tool == 'time':

src/sinol_make/oiejq/__init__.py

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
import os
2+
import subprocess
3+
import sys
4+
import tarfile
5+
import tempfile
6+
import requests
7+
8+
from sinol_make import util
9+
10+
11+
def check_oiejq(path = None):
12+
"""
13+
Function to check if oiejq is installed
14+
"""
15+
if sys.platform != 'linux':
16+
return False
17+
18+
def check(path):
19+
try:
20+
p = subprocess.Popen([path], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
21+
p.wait()
22+
p.kill()
23+
return p.returncode == 0
24+
except FileNotFoundError:
25+
return False
26+
27+
if path is not None:
28+
return check(path)
29+
30+
if not check(os.path.expanduser('~/.local/bin/oiejq')):
31+
return False
32+
else:
33+
return True
34+
35+
36+
def install_oiejq():
37+
"""
38+
Function to install oiejq, if not installed.
39+
Returns True if successful.
40+
"""
41+
42+
if sys.platform != 'linux':
43+
return False
44+
if check_oiejq():
45+
return True
46+
47+
if not os.path.exists(os.path.expanduser('~/.local/bin')):
48+
os.makedirs(os.path.expanduser('~/.local/bin'), exist_ok=True)
49+
50+
try:
51+
request = requests.get('https://oij.edu.pl/zawodnik/srodowisko/oiejq.tar.gz')
52+
except requests.exceptions.ConnectionError:
53+
raise Exception('Couldn\'t download oiejq (https://oij.edu.pl/zawodnik/srodowisko/oiejq.tar.gz couldn\'t connect)')
54+
if request.status_code != 200:
55+
raise Exception('Couldn\'t download oiejq (https://oij.edu.pl/zawodnik/srodowisko/oiejq.tar.gz returned status code: ' + str(request.status_code) + ')')
56+
57+
# oiejq is downloaded to a temporary directory and not to the `cache` dir,
58+
# as there is no guarantee that the current directory is the package directory.
59+
# The `cache` dir is only used for files that are part of the package and those
60+
# that the package creator might want to look into.
61+
with tempfile.TemporaryDirectory() as tmpdir:
62+
oiejq_path = os.path.join(tmpdir, 'oiejq.tar.gz')
63+
with open(oiejq_path, 'wb') as oiejq_file:
64+
oiejq_file.write(request.content)
65+
66+
def strip(tar):
67+
l = len('oiejq/')
68+
for member in tar.getmembers():
69+
member.name = member.name[l:]
70+
yield member
71+
72+
with tarfile.open(oiejq_path) as tar:
73+
tar.extractall(path=os.path.expanduser('~/.local/bin'), members=strip(tar))
74+
os.rename(os.path.expanduser('~/.local/bin/oiejq.sh'), os.path.expanduser('~/.local/bin/oiejq'))
75+
76+
return check_oiejq()
77+
78+
79+
def get_oiejq_path():
80+
if not check_oiejq():
81+
return None
82+
83+
def check(path):
84+
p = subprocess.Popen([path], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
85+
p.wait()
86+
p.kill()
87+
if p.returncode == 0:
88+
return True
89+
else:
90+
return False
91+
92+
if check(os.path.expanduser('~/.local/bin/oiejq')):
93+
return os.path.expanduser('~/.local/bin/oiejq')
94+
else:
95+
return None
96+
97+
98+
def check_perf_counters_enabled():
99+
"""
100+
Checks if `kernel.perf_event_paranoid` is set to -1.
101+
:return:
102+
"""
103+
if sys.platform != 'linux' or not check_oiejq():
104+
return
105+
106+
oiejq = get_oiejq_path()
107+
test_file = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'perf_test.py')
108+
python_executable = sys.executable
109+
110+
# subprocess.Pipe is not used, because than the code would hang on process.communicate()
111+
with tempfile.TemporaryFile() as tmpfile:
112+
process = subprocess.Popen([oiejq, python_executable, test_file], stdout=tmpfile, stderr=subprocess.DEVNULL)
113+
process.wait()
114+
tmpfile.seek(0)
115+
output = tmpfile.read().decode('utf-8')
116+
process.terminate()
117+
118+
if output != "Test string\n":
119+
util.exit_with_error("To use the recommended tool for measuring time called oiejq, please:\n"
120+
"- execute `sudo sysctl kernel.perf_event_paranoid=-1` to make oiejq work for\n"
121+
" the current system session,\n"
122+
"- or add `kernel.perf_event_paranoid=-1` to `/etc/sysctl.conf`\n"
123+
" and reboot to permanently make oiejq work.\n"
124+
"For more details, see https://github.com/sio2project/sio2jail#running.\n")

src/sinol_make/oiejq/perf_test.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
print("Test string")

src/sinol_make/util.py

Lines changed: 1 addition & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
import glob, importlib, os, sys, subprocess, requests, tarfile, yaml
2-
import tempfile
1+
import glob, importlib, os, sys, requests, yaml
32
import threading
43
from typing import Union
54

@@ -41,94 +40,6 @@ def exit_if_not_package():
4140
exit_with_error('You are not in a package directory (couldn\'t find config.yml in current directory).')
4241

4342

44-
def check_oiejq(path = None):
45-
"""
46-
Function to check if oiejq is installed
47-
"""
48-
if sys.platform != 'linux':
49-
return False
50-
51-
def check(path):
52-
try:
53-
p = subprocess.Popen([path], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
54-
p.wait()
55-
if p.returncode == 0:
56-
return True
57-
else:
58-
return False
59-
except FileNotFoundError:
60-
return False
61-
62-
if path is not None:
63-
return check(path)
64-
65-
if not check(os.path.expanduser('~/.local/bin/oiejq')):
66-
return False
67-
else:
68-
return True
69-
70-
71-
def install_oiejq():
72-
"""
73-
Function to install oiejq, if not installed.
74-
Returns True if successful.
75-
"""
76-
77-
if sys.platform != 'linux':
78-
return False
79-
if check_oiejq():
80-
return True
81-
82-
if not os.path.exists(os.path.expanduser('~/.local/bin')):
83-
os.makedirs(os.path.expanduser('~/.local/bin'), exist_ok=True)
84-
85-
try:
86-
request = requests.get('https://oij.edu.pl/zawodnik/srodowisko/oiejq.tar.gz')
87-
except requests.exceptions.ConnectionError:
88-
raise Exception('Couldn\'t download oiejq (https://oij.edu.pl/zawodnik/srodowisko/oiejq.tar.gz couldn\'t connect)')
89-
if request.status_code != 200:
90-
raise Exception('Couldn\'t download oiejq (https://oij.edu.pl/zawodnik/srodowisko/oiejq.tar.gz returned status code: ' + str(request.status_code) + ')')
91-
92-
# oiejq is downloaded to a temporary directory and not to the `cache` dir,
93-
# as there is no guarantee that the current directory is the package directory.
94-
# The `cache` dir is only used for files that are part of the package and those
95-
# that the package creator might want to look into.
96-
with tempfile.TemporaryDirectory() as tmpdir:
97-
oiejq_path = os.path.join(tmpdir, 'oiejq.tar.gz')
98-
with open(oiejq_path, 'wb') as oiejq_file:
99-
oiejq_file.write(request.content)
100-
101-
def strip(tar):
102-
l = len('oiejq/')
103-
for member in tar.getmembers():
104-
member.name = member.name[l:]
105-
yield member
106-
107-
with tarfile.open(oiejq_path) as tar:
108-
tar.extractall(path=os.path.expanduser('~/.local/bin'), members=strip(tar))
109-
os.rename(os.path.expanduser('~/.local/bin/oiejq.sh'), os.path.expanduser('~/.local/bin/oiejq'))
110-
111-
return check_oiejq()
112-
113-
114-
def get_oiejq_path():
115-
if not check_oiejq():
116-
return None
117-
118-
def check(path):
119-
p = subprocess.Popen([path, "--version"], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
120-
p.wait()
121-
if p.returncode == 0:
122-
return True
123-
else:
124-
return False
125-
126-
if check(os.path.expanduser('~/.local/bin/oiejq')):
127-
return os.path.expanduser('~/.local/bin/oiejq')
128-
else:
129-
return None
130-
131-
13243
def save_config(config):
13344
"""
13445
Function to save nicely formated config.yml.

tests/commands/run/test_unit.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import argparse, re, yaml
2-
from sinol_make import util
2+
3+
from sinol_make import util, oiejq
34
from sinol_make.commands.run.structs import ResultChange, ValidationResult
45
from sinol_make.structs.status_structs import Status
56
from sinol_make.helpers import package_util
@@ -55,7 +56,7 @@ def test_execution(create_package, time_tool):
5556
config = yaml.load(config_file, Loader=yaml.FullLoader)
5657

5758
os.makedirs(os.path.join(command.EXECUTIONS_DIR, solution), exist_ok=True)
58-
result = command.run_solution((solution, os.path.join(command.EXECUTABLES_DIR, executable), test, config['time_limit'], config['memory_limit'], util.get_oiejq_path()))
59+
result = command.run_solution((solution, os.path.join(command.EXECUTABLES_DIR, executable), test, config['time_limit'], config['memory_limit'], oiejq.get_oiejq_path()))
5960
assert result.Status == Status.OK
6061

6162

@@ -83,7 +84,7 @@ def test_run_solutions(create_package, time_tool):
8384
command.possible_score = command.get_possible_score(command.groups)
8485
command.memory_limit = command.config["memory_limit"]
8586
command.time_limit = command.config["time_limit"]
86-
command.timetool_path = util.get_oiejq_path()
87+
command.timetool_path = oiejq.get_oiejq_path()
8788

8889
def flatten_results(results):
8990
new_results = {}

tests/conftest.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,3 +35,9 @@ def pytest_collection_modifyitems(config, items: List[pytest.Item]):
3535
for item in items:
3636
if "github_runner" in item.keywords:
3737
item.add_marker(pytest.mark.skip(reason="only for GitHub runner"))
38+
39+
for item in items:
40+
if "oiejq" in item.keywords:
41+
if sys.platform != "linux" or config.getoption("--time-tool") == ["time"] or \
42+
config.getoption("--github-runner"):
43+
item.add_marker(pytest.mark.skip(reason="oiejq required"))

tests/test_oiejq.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import os
2+
import sys
3+
import pytest
4+
5+
from sinol_make import oiejq
6+
7+
8+
@pytest.mark.github_runner
9+
def test_install_oiejq():
10+
if sys.platform != 'linux':
11+
return
12+
13+
if oiejq.check_oiejq():
14+
os.remove(os.path.expanduser('~/.local/bin/oiejq'))
15+
assert not oiejq.check_oiejq()
16+
17+
assert oiejq.install_oiejq()
18+
19+
20+
@pytest.mark.github_runner
21+
def test_perf_counters_not_set():
22+
"""
23+
Test `oiejq.check_perf_counters_enabled` with perf counters disabled
24+
"""
25+
if sys.platform != 'linux':
26+
return
27+
28+
with pytest.raises(SystemExit):
29+
oiejq.check_perf_counters_enabled()
30+
31+
32+
@pytest.mark.oiejq
33+
def test_perf_counters_set():
34+
"""
35+
Test `oiejq.check_perf_counters_enabled` with perf counters enabled
36+
"""
37+
oiejq.check_perf_counters_enabled()

tests/test_util.py

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,24 +5,11 @@
55
import tempfile
66
import requests
77
import requests_mock
8-
98
import pytest
109

1110
from sinol_make import util
1211

1312

14-
@pytest.mark.github_runner
15-
def test_install_oiejq():
16-
if sys.platform != 'linux':
17-
return
18-
19-
if util.check_oiejq():
20-
os.remove(sys.path.expanduser('~/.local/bin/oiejq'))
21-
assert not util.check_oiejq()
22-
23-
assert util.install_oiejq()
24-
25-
2613
def test_file_diff():
2714
with tempfile.TemporaryDirectory() as tmpdir:
2815
a_file = os.path.join(tmpdir, 'a')

0 commit comments

Comments
 (0)