Skip to content

Commit 1abbcf3

Browse files
ekzhusonichi
andauthored
Update tests for code_utils and use ThreadPoolExecutor for code execution in windows (#1472)
* threadpoolexecutor * update tests * update tests * fix tests on windows * fix test * update tests * update tests * update tests * update tests * update tests * update tests * fix build.yml * build yaml * test * use skip-docker to explicitly skipping docker tests * update tests * contribution doc update * Update website/docs/Contribute.md Co-authored-by: Chi Wang <[email protected]> * update doc --------- Co-authored-by: Chi Wang <[email protected]>
1 parent 392d381 commit 1abbcf3

File tree

5 files changed

+140
-123
lines changed

5 files changed

+140
-123
lines changed

.github/workflows/build.yml

+9-13
Original file line numberDiff line numberDiff line change
@@ -20,17 +20,15 @@ concurrency:
2020
group: ${{ github.workflow }}-${{ github.ref }}-${{ github.head_ref }}
2121
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
2222
permissions: {}
23-
# actions: read
24-
# checks: read
25-
# contents: read
26-
# deployments: read
2723
jobs:
2824
build:
2925
runs-on: ${{ matrix.os }}
26+
env:
27+
AUTOGEN_USE_DOCKER: ${{ matrix.os != 'ubuntu-latest' && 'False' }}
3028
strategy:
3129
fail-fast: false
3230
matrix:
33-
os: [ubuntu-latest, macos-latest, windows-2019]
31+
os: [ubuntu-latest, macos-latest, windows-latest]
3432
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
3533
steps:
3634
- uses: actions/checkout@v3
@@ -44,16 +42,14 @@ jobs:
4442
pip install -e .
4543
python -c "import autogen"
4644
pip install pytest mock
47-
- name: Set AUTOGEN_USE_DOCKER based on OS
48-
shell: bash
49-
run: |
50-
if [[ ${{ matrix.os }} != ubuntu-latest ]]; then
51-
echo "AUTOGEN_USE_DOCKER=False" >> $GITHUB_ENV
52-
fi
53-
- name: Test with pytest
54-
if: matrix.python-version != '3.10'
45+
- name: Test with pytest skipping openai tests
46+
if: matrix.python-version != '3.10' && matrix.os == 'ubuntu-latest'
5547
run: |
5648
pytest test --skip-openai
49+
- name: Test with pytest skipping openai and docker tests
50+
if: matrix.python-version != '3.10' && matrix.os != 'ubuntu-latest'
51+
run: |
52+
pytest test --skip-openai --skip-docker
5753
- name: Coverage
5854
if: matrix.python-version == '3.10'
5955
run: |

autogen/code_utils.py

+10-24
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,7 @@
1212

1313
from autogen import oai
1414

15-
try:
16-
import docker
17-
except ImportError:
18-
docker = None
15+
import docker
1916

2017
SENTINEL = object()
2118
DEFAULT_MODEL = "gpt-4"
@@ -232,8 +229,6 @@ def is_docker_running():
232229
Returns:
233230
bool: True if docker is running; False otherwise.
234231
"""
235-
if docker is None:
236-
return False
237232
try:
238233
client = docker.from_env()
239234
client.ping()
@@ -394,29 +389,20 @@ def execute_code(
394389
sys.executable if lang.startswith("python") else _cmd(lang),
395390
f".\\{filename}" if WIN32 else filename,
396391
]
397-
if WIN32:
398-
logger.warning("SIGALRM is not supported on Windows. No timeout will be enforced.")
399-
result = subprocess.run(
392+
with ThreadPoolExecutor(max_workers=1) as executor:
393+
future = executor.submit(
394+
subprocess.run,
400395
cmd,
401396
cwd=work_dir,
402397
capture_output=True,
403398
text=True,
404399
)
405-
else:
406-
with ThreadPoolExecutor(max_workers=1) as executor:
407-
future = executor.submit(
408-
subprocess.run,
409-
cmd,
410-
cwd=work_dir,
411-
capture_output=True,
412-
text=True,
413-
)
414-
try:
415-
result = future.result(timeout=timeout)
416-
except TimeoutError:
417-
if original_filename is None:
418-
os.remove(filepath)
419-
return 1, TIMEOUT_MSG, None
400+
try:
401+
result = future.result(timeout=timeout)
402+
except TimeoutError:
403+
if original_filename is None:
404+
os.remove(filepath)
405+
return 1, TIMEOUT_MSG, None
420406
if original_filename is None:
421407
os.remove(filepath)
422408
if result.returncode:

test/conftest.py

+6-2
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,23 @@
22

33
skip_openai = False
44
skip_redis = False
5+
skip_docker = False
56

67

7-
# Registers command-line option '--skip-openai' and '--skip-redis' via pytest hook.
8+
# Registers command-line options like '--skip-openai' and '--skip-redis' via pytest hook.
89
# When these flags are set, it indicates that tests requiring OpenAI or Redis (respectively) should be skipped.
910
def pytest_addoption(parser):
1011
parser.addoption("--skip-openai", action="store_true", help="Skip all tests that require openai")
1112
parser.addoption("--skip-redis", action="store_true", help="Skip all tests that require redis")
13+
parser.addoption("--skip-docker", action="store_true", help="Skip all tests that require docker")
1214

1315

14-
# pytest hook implementation extracting the '--skip-openai' and '--skip-redis' command line arg and exposing it globally
16+
# pytest hook implementation extracting command line args and exposing it globally
1517
@pytest.hookimpl(tryfirst=True)
1618
def pytest_configure(config):
1719
global skip_openai
1820
skip_openai = config.getoption("--skip-openai", False)
1921
global skip_redis
2022
skip_redis = config.getoption("--skip-redis", False)
23+
global skip_docker
24+
skip_docker = config.getoption("--skip-docker", False)

test/test_code.py

+91-75
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,12 @@
1-
import importlib.metadata
21
import os
3-
import sys
2+
import tempfile
43
import unittest
54

65
import pytest
76

87
import autogen
98
from autogen.code_utils import (
10-
PATH_SEPARATOR,
119
UNKNOWN,
12-
WIN32,
1310
content_str,
1411
execute_code,
1512
extract_code,
@@ -21,27 +18,13 @@
2118
decide_use_docker,
2219
check_can_use_docker_or_throw,
2320
)
21+
from conftest import skip_docker
2422

2523
KEY_LOC = "notebook"
2624
OAI_CONFIG_LIST = "OAI_CONFIG_LIST"
2725
here = os.path.abspath(os.path.dirname(__file__))
2826

2927

30-
def is_package_installed(package_name):
31-
"""Check if a package is installed. This is a preferred way to check if a
32-
package is installed or not than doing a try-catch around an import
33-
because it avoids name conflict with local modules and
34-
code execution in the imported module."""
35-
try:
36-
importlib.metadata.version(package_name)
37-
return True
38-
except importlib.metadata.PackageNotFoundError:
39-
return False
40-
41-
42-
docker_package_installed = is_package_installed("docker")
43-
44-
4528
# def test_find_code():
4629
# try:
4730
# import openai
@@ -313,86 +296,119 @@ def scrape(url):
313296
assert len(codeblocks) == 1 and codeblocks[0] == ("", "source setup.sh")
314297

315298

316-
# skip if os is windows
317299
@pytest.mark.skipif(
318-
sys.platform in ["win32"] or (not is_docker_running() and not in_docker_container()), reason="docker is not running"
300+
skip_docker or not is_docker_running(),
301+
reason="docker is not running or requested to skip docker tests",
319302
)
320-
def test_execute_code(use_docker=None):
321-
try:
322-
import docker
323-
except ImportError as exc:
324-
print(exc)
325-
docker = None
326-
if use_docker is None:
327-
use_docker = docker is not None
328-
exit_code, msg, image = execute_code("print('hello world')", filename="tmp/codetest.py", use_docker=use_docker)
329-
assert exit_code == 0 and msg == "hello world\n", msg
330-
# read a file
331-
print(execute_code("with open('tmp/codetest.py', 'r') as f: a=f.read()", use_docker=use_docker))
332-
# create a file
333-
exit_code, msg, image = execute_code(
334-
"with open('tmp/codetest.py', 'w') as f: f.write('b=1')",
335-
work_dir=f"{here}/my_tmp",
336-
filename="tmp2/codetest.py",
337-
use_docker=use_docker,
338-
)
339-
assert exit_code and (
340-
'File "tmp2/codetest.py"'.replace("/", PATH_SEPARATOR) in msg
341-
or 'File ".\\tmp2/codetest.py' in msg # py3.8 + win32
342-
), msg
343-
print(
344-
execute_code(
345-
"with open('tmp/codetest.py', 'w') as f: f.write('b=1')", work_dir=f"{here}/my_tmp", use_docker=use_docker
303+
def test_execute_code(use_docker=True):
304+
# Test execute code and save the code to a file.
305+
with tempfile.TemporaryDirectory() as tempdir:
306+
filename = "temp_file_with_code.py"
307+
308+
# execute code and save the code to a file.
309+
exit_code, msg, image = execute_code(
310+
"print('hello world')",
311+
filename=filename,
312+
work_dir=tempdir,
313+
use_docker=use_docker,
346314
)
347-
)
348-
# execute code in a file
349-
print(execute_code(filename="tmp/codetest.py", use_docker=use_docker))
350-
print(execute_code("python tmp/codetest.py", lang="sh", use_docker=use_docker))
351-
# execute code for assertion error
352-
exit_code, msg, image = execute_code("assert 1==2", use_docker=use_docker)
353-
assert exit_code, msg
354-
assert 'File ""' in msg or 'File ".\\"' in msg # py3.8 + win32
355-
# execute code which takes a long time
356-
exit_code, error, image = execute_code("import time; time.sleep(2)", timeout=1, use_docker=use_docker)
357-
assert exit_code and error == "Timeout" or WIN32
358-
assert isinstance(image, str) or docker is None or os.path.exists("/.dockerenv") or use_docker is False
315+
assert exit_code == 0 and msg == "hello world\n", msg
316+
317+
# read the file just saved
318+
exit_code, msg, image = execute_code(
319+
f"with open('{filename}', 'rt') as f: print(f.read())",
320+
use_docker=use_docker,
321+
work_dir=tempdir,
322+
)
323+
assert exit_code == 0 and "print('hello world')" in msg, msg
324+
325+
# execute code in a file
326+
exit_code, msg, image = execute_code(
327+
filename=filename,
328+
use_docker=use_docker,
329+
work_dir=tempdir,
330+
)
331+
assert exit_code == 0 and msg == "hello world\n", msg
332+
333+
# execute code in a file using shell command directly
334+
exit_code, msg, image = execute_code(
335+
f"python {filename}",
336+
lang="sh",
337+
use_docker=use_docker,
338+
work_dir=tempdir,
339+
)
340+
assert exit_code == 0 and msg == "hello world\n", msg
341+
342+
with tempfile.TemporaryDirectory() as tempdir:
343+
# execute code for assertion error
344+
exit_code, msg, image = execute_code(
345+
"assert 1==2",
346+
use_docker=use_docker,
347+
work_dir=tempdir,
348+
)
349+
assert exit_code, msg
350+
assert "AssertionError" in msg
351+
assert 'File "' in msg or 'File ".\\"' in msg # py3.8 + win32
352+
353+
with tempfile.TemporaryDirectory() as tempdir:
354+
# execute code which takes a long time
355+
exit_code, error, image = execute_code(
356+
"import time; time.sleep(2)",
357+
timeout=1,
358+
use_docker=use_docker,
359+
work_dir=tempdir,
360+
)
361+
assert exit_code and error == "Timeout"
362+
if use_docker is True:
363+
assert isinstance(image, str)
359364

360365

361366
@pytest.mark.skipif(
362-
sys.platform in ["win32"] or (not is_docker_running()) or in_docker_container(),
363-
reason="docker is not running or in docker container already",
367+
skip_docker or not is_docker_running(),
368+
reason="docker is not running or requested to skip docker tests",
364369
)
365370
def test_execute_code_with_custom_filename_on_docker():
366-
exit_code, msg, image = execute_code("print('hello world')", filename="tmp/codetest.py", use_docker=True)
367-
assert exit_code == 0 and msg == "hello world\n", msg
368-
assert image == "python:tmp_codetest.py"
371+
with tempfile.TemporaryDirectory() as tempdir:
372+
filename = "codetest.py"
373+
exit_code, msg, image = execute_code(
374+
"print('hello world')",
375+
filename=filename,
376+
use_docker=True,
377+
work_dir=tempdir,
378+
)
379+
assert exit_code == 0 and msg == "hello world\n", msg
380+
assert image == "python:codetest.py"
369381

370382

371383
@pytest.mark.skipif(
372-
sys.platform in ["win32"] or (not is_docker_running()) or in_docker_container(),
373-
reason="docker is not running or in docker container already",
384+
skip_docker or not is_docker_running(),
385+
reason="docker is not running or requested to skip docker tests",
374386
)
375387
def test_execute_code_with_misformed_filename_on_docker():
376-
exit_code, msg, image = execute_code(
377-
"print('hello world')", filename="tmp/codetest.py (some extra information)", use_docker=True
378-
)
379-
assert exit_code == 0 and msg == "hello world\n", msg
380-
assert image == "python:tmp_codetest.py__some_extra_information_"
388+
with tempfile.TemporaryDirectory() as tempdir:
389+
filename = "codetest.py (some extra information)"
390+
exit_code, msg, image = execute_code(
391+
"print('hello world')",
392+
filename=filename,
393+
use_docker=True,
394+
work_dir=tempdir,
395+
)
396+
assert exit_code == 0 and msg == "hello world\n", msg
397+
assert image == "python:codetest.py__some_extra_information_"
381398

382399

383400
def test_execute_code_raises_when_code_and_filename_are_both_none():
384401
with pytest.raises(AssertionError):
385402
execute_code(code=None, filename=None)
386403

387404

388-
def test_execute_code_nodocker():
405+
def test_execute_code_no_docker():
389406
test_execute_code(use_docker=False)
390407

391408

392-
def test_execute_code_no_docker():
409+
def test_execute_code_timeout_no_docker():
393410
exit_code, error, image = execute_code("import time; time.sleep(2)", timeout=1, use_docker=False)
394-
if sys.platform != "win32":
395-
assert exit_code and error == "Timeout"
411+
assert exit_code and error == "Timeout"
396412
assert image is None
397413

398414

website/docs/Contribute.md

+24-9
Original file line numberDiff line numberDiff line change
@@ -155,23 +155,38 @@ Tests are automatically run via GitHub actions. There are two workflows:
155155

156156
The first workflow is required to pass for all PRs (and it doesn't do any OpenAI calls). The second workflow is required for changes that affect the OpenAI tests (and does actually call LLM). The second workflow requires approval to run. When writing tests that require OpenAI calls, please use [`pytest.mark.skipif`](https://github.com/microsoft/autogen/blob/b1adac515931bf236ac59224269eeec683a162ba/test/oai/test_client.py#L19) to make them run in only when `openai` package is installed. If additional dependency for this test is required, install the dependency in the corresponding python version in [openai.yml](https://github.com/microsoft/autogen/blob/main/.github/workflows/openai.yml).
157157

158-
#### Run non-OpenAI tests
158+
Make sure all tests pass, this is required for [build.yml](https://github.com/microsoft/autogen/blob/main/.github/workflows/build.yml) checks to pass
159159

160-
To run the subset of the tests not depending on `openai` (and not calling LLMs)):
160+
#### Running tests locally
161161

162-
- Install pytest:
162+
To run tests, install the [test] option:
163163

164-
``` code
165-
pip install pytest
164+
```bash
165+
pip install -e."[test]"
166166
```
167167

168-
- Run the tests from the `test` folder using the `--skip-openai` flag.
168+
Then you can run the tests from the `test` folder using the following command:
169169

170-
``` code
171-
pytest test --skip-openai
170+
```bash
171+
pytest test
172172
```
173173

174-
- Make sure all tests pass, this is required for [build.yml](https://github.com/microsoft/autogen/blob/main/.github/workflows/build.yml) checks to pass
174+
Tests for the `autogen.agentchat.contrib` module may be skipped automatically if the
175+
required dependencies are not installed. Please consult the documentation for
176+
each contrib module to see what dependencies are required.
177+
178+
#### Skip flags for tests
179+
180+
- `--skip-openai` for skipping tests that require access to OpenAI services.
181+
- `--skip-docker` for skipping tests that explicitly use docker
182+
- `--skip-redis` for skipping tests that require a Redis server
183+
184+
For example, the following command will skip tests that require access to
185+
OpenAI and docker services:
186+
187+
```bash
188+
pytest test --skip-openai --skip-docker
189+
```
175190

176191
### Coverage
177192

0 commit comments

Comments
 (0)