Skip to content

Commit ea4d42e

Browse files
KavyaSree2610Kavya Sree Kaitepalli0xba1a
authored
Rewrite test directory and add github workflow to run test (#43)
* add integration and unit tests for reading bot * Rewrite tests for bots * Rewrite docker environment tests * Add workflow for tests * Change model for brwsing bot * set env variables * RUn for only 1 python version * make to run only integration tests * remove docker tests step * Correct syntax error * add heredoc execution test * Update ShellCommunicator to handle heredoc commands * add codecov * change branch to main * Apply suggestion from @0xba1a * Apply suggestion from @0xba1a * Apply suggestion from @0xba1a * Apply suggestion from @0xba1a --------- Co-authored-by: Kavya Sree Kaitepalli <kkaitepalli@microsoft.com> Co-authored-by: bala <kumaran.4353@gmail.com>
1 parent 17efa07 commit ea4d42e

18 files changed

Lines changed: 1197 additions & 274 deletions

File tree

.codecov.yml

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
coverage:
2+
status:
3+
project:
4+
default:
5+
target: auto
6+
threshold: 1%
7+
informational: true
8+
patch:
9+
default:
10+
target: 100%
11+
threshold: 1%
12+
informational: false
13+
14+
comment:
15+
layout: "reach,diff,flags,tree"
16+
behavior: default
17+
require_changes: false
18+
19+
ignore:
20+
- "docs/"
21+
- "test/"
22+
- "**/test_*.py"
23+
- "setup.py"
24+
- "conftest.py"
25+
- "README.md"
26+
- "LICENSE"

.github/workflows/test.yml

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
name: Run Tests
2+
3+
on:
4+
pull_request:
5+
branches: [ "main" ]
6+
workflow_dispatch:
7+
# Allow manual triggering
8+
9+
jobs:
10+
test:
11+
runs-on: ubuntu-latest
12+
strategy:
13+
matrix:
14+
test-type: ["integration"]
15+
include:
16+
- test-type: "integration"
17+
pytest-args: "-m 'integration'"
18+
19+
services:
20+
docker:
21+
image: docker:dind
22+
options: --privileged
23+
24+
steps:
25+
- name: Checkout code
26+
uses: actions/checkout@v4
27+
28+
- name: Set up Python 3.12
29+
uses: actions/setup-python@v5
30+
with:
31+
python-version: "3.12"
32+
33+
- name: Set up Docker Buildx
34+
if: matrix.test-type == 'integration'
35+
uses: docker/setup-buildx-action@v3
36+
37+
- name: Cache pip dependencies
38+
uses: actions/cache@v4
39+
with:
40+
path: ~/.cache/pip
41+
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
42+
restore-keys: |
43+
${{ runner.os }}-pip-
44+
45+
- name: Install system dependencies
46+
run: |
47+
sudo apt-get update
48+
sudo apt-get install -y build-essential
49+
50+
- name: Install Python dependencies
51+
run: |
52+
python -m pip install --upgrade pip
53+
pip install -r requirements.txt
54+
pip install pytest pytest-cov pytest-mock pytest-asyncio
55+
56+
- name: Install package in development mode
57+
run: |
58+
pip install -e .
59+
60+
- name: Build Docker images for integration tests
61+
if: matrix.test-type == 'integration'
62+
run: |
63+
# Build the shell server image needed for Docker tests
64+
docker build -f src/microbots/environment/local_docker/image_builder/Dockerfile -t kavyasree261002/shell_server:latest .
65+
66+
- name: Run ${{ matrix.test-type }} tests
67+
env:
68+
# OpenAI API Configuration
69+
OPEN_AI_KEY: ${{ secrets.OPEN_AI_KEY }}
70+
OPEN_AI_DEPLOYMENT_NAME: ${{ secrets.OPEN_AI_DEPLOYMENT_NAME }}
71+
OPEN_AI_END_POINT: ${{ secrets.OPEN_AI_END_POINT }}
72+
# Azure OpenAI API Configuration
73+
AZURE_OPENAI_API_VERSION: ${{ secrets.AZURE_OPENAI_API_VERSION }}
74+
AZURE_OPENAI_API_KEY: ${{ secrets.AZURE_OPENAI_API_KEY }}
75+
AZURE_OPENAI_ENDPOINT: ${{ secrets.AZURE_OPENAI_ENDPOINT }}
76+
run: |
77+
python -m pytest ${{ matrix.pytest-args }} \
78+
--cov=src \
79+
--cov-report=xml \
80+
--cov-report=term-missing \
81+
--junitxml=test-results-${{ matrix.test-type }}.xml \
82+
-v
83+
84+
- name: Upload test results
85+
uses: actions/upload-artifact@v4
86+
if: always()
87+
with:
88+
name: test-results-${{ matrix.test-type }}
89+
path: test-results-*.xml
90+
91+
- name: Upload coverage reports
92+
uses: actions/upload-artifact@v4
93+
if: always()
94+
with:
95+
name: coverage-${{ matrix.test-type }}
96+
path: coverage.xml
97+
98+
- name: Upload coverage to Codecov
99+
uses: codecov/codecov-action@v4
100+
if: always()
101+
with:
102+
token: ${{ secrets.CODECOV_TOKEN }}
103+
file: ./coverage.xml
104+
flags: ${{ matrix.test-type }}
105+
name: codecov-${{ matrix.test-type }}
106+
fail_ci_if_error: false
107+
108+
test-summary:
109+
runs-on: ubuntu-latest
110+
needs: [test]
111+
if: always()
112+
steps:
113+
- name: Download all test results
114+
uses: actions/download-artifact@v4
115+
with:
116+
pattern: test-results-*
117+
merge-multiple: true
118+
119+
- name: Test Summary
120+
if: always()
121+
run: |
122+
echo "## Test Results Summary" >> $GITHUB_STEP_SUMMARY
123+
echo "| Test Type | Status |" >> $GITHUB_STEP_SUMMARY
124+
echo "|-----------|--------|" >> $GITHUB_STEP_SUMMARY
125+
if [ "${{ needs.test.result }}" = "success" ]; then
126+
echo "| Integration Tests | ✅ Passed |" >> $GITHUB_STEP_SUMMARY
127+
else
128+
echo "| Integration Tests | ❌ Failed |" >> $GITHUB_STEP_SUMMARY
129+
fi

pytest.ini

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,15 @@
1+
[tool:pytest]
2+
testpaths = test
3+
python_files = test_*.py
4+
python_functions = test_*
5+
addopts =
6+
-v
7+
--tb=short
8+
--strict-markers
9+
110
[pytest]
211
markers =
12+
unit: Unit tests
13+
integration: Integration tests
14+
slow: Slow tests
315
docker: marks tests that require a running Docker daemon and pull container images
4-

src/microbots/environment/local_docker/LocalDockerEnvironment.py

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -130,10 +130,18 @@ def stop(self):
130130
except Exception as e:
131131
logger.error("❌ Failed to remove working directory: %s", e)
132132

133+
# Unused function. Keeping for reference or future use
134+
def _escape(self, command: str) -> str:
135+
# Escape double quotes and special characters for JSON safety
136+
command = command.replace('"', '\\"')
137+
command = command.replace("<", "&lt;").replace(">", "&gt;")
138+
return command
139+
133140
def execute(
134141
self, command: str, timeout: Optional[int] = 300
135142
) -> CmdReturn: # TODO: Need proper return value
136143
logger.debug("➡️ Executing command in container: %s", command)
144+
# command = self._escape(command)
137145
try:
138146
response = requests.post(
139147
f"http://localhost:{self.port}/",
@@ -163,11 +171,11 @@ def execute(
163171
def copy_to_container(self, src_path: str, dest_path: str) -> bool:
164172
"""
165173
Copy a file or folder from the host machine to the Docker container.
166-
174+
167175
Args:
168176
src_path: Path to the source file/folder on the host machine
169177
dest_path: Destination path inside the container
170-
178+
171179
Returns:
172180
bool: True if copy was successful, False otherwise
173181
"""
@@ -193,7 +201,7 @@ def copy_to_container(self, src_path: str, dest_path: str) -> bool:
193201
mkdir_result = self.execute(mkdir_cmd)
194202

195203
if mkdir_result.return_code != 0:
196-
logger.error("❌ Failed to create destination directory %s: %s",
204+
logger.error("❌ Failed to create destination directory %s: %s",
197205
dest_dir, mkdir_result.stderr)
198206
return False
199207
else:
@@ -212,7 +220,6 @@ def copy_to_container(self, src_path: str, dest_path: str) -> bool:
212220
# Execute the copy command
213221
result = subprocess.run(
214222
cmd,
215-
shell=True,
216223
capture_output=True,
217224
text=True,
218225
timeout=300
@@ -235,11 +242,11 @@ def copy_to_container(self, src_path: str, dest_path: str) -> bool:
235242
def copy_from_container(self, src_path: str, dest_path: str) -> bool:
236243
"""
237244
Copy a file or folder from the Docker container to the host machine.
238-
245+
239246
Args:
240247
src_path: Path to the source file/folder inside the container
241248
dest_path: Destination path on the host machine
242-
249+
243250
Returns:
244251
bool: True if copy was successful, False otherwise
245252
"""
@@ -271,7 +278,6 @@ def copy_from_container(self, src_path: str, dest_path: str) -> bool:
271278
# Execute the copy command
272279
result = subprocess.run(
273280
cmd,
274-
shell=True,
275281
capture_output=True,
276282
text=True,
277283
timeout=300

src/microbots/environment/local_docker/image_builder/ShellCommunicator.py

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
from dataclasses import dataclass
1717

1818
logger = logging.getLogger(__name__)
19+
logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', filename='/var/log/ShellCommunicator.log')
1920

2021
@dataclass
2122
class CmdReturn:
@@ -126,12 +127,20 @@ def _monitor_output(self, stream, output_queue: queue.Queue, stream_type: str):
126127
except Exception as e:
127128
output_queue.put((stream_type, f"Monitor error: {e}"))
128129

130+
# Unused function. Keeping for reference and future use
129131
def _re_escape(self, command: str) -> str:
130132
# Reverse .replace('"', '\\"')
131133
command = command.replace('\"', '"')
132-
# command = command.replace("&lt;", "<").replace("&gt;", ">")
134+
command = command.replace("&lt;", "<").replace("&gt;", ">")
133135
return command
134136

137+
# Unused function. Keeping for reference and future use
138+
def _is_heredoc_command(self, command: str) -> bool:
139+
"""Check if command contains heredoc syntax."""
140+
import re
141+
# Look for heredoc patterns like <<EOF, <<'END', <<"DELIMITER", etc.
142+
return bool(re.search(r'<<\s*[\'"]?[A-Za-z_][A-Za-z0-9_]*[\'"]?', command))
143+
135144
def send_command(
136145
self, command: str, wait_for_output: bool = True, timeout: float = 300
137146
) -> CmdReturn:
@@ -151,8 +160,8 @@ def send_command(
151160
return CmdReturn(stdout="", stderr="No active shell session", return_code=1)
152161

153162
try:
154-
command = self._re_escape(command)
155-
163+
# command = self._re_escape(command)
164+
156165
if not wait_for_output:
157166
# Send the command without marker for async execution
158167
self.process.stdin.write(command + "\n")
@@ -163,13 +172,14 @@ def send_command(
163172
# Generate a unique command completion marker
164173
marker = f"__COMMAND_COMPLETE_{int(time.time() * 1000000)}__"
165174

166-
# For bash only: Send command + marker in a single line to capture correct exit code
167-
combined_command = f"{command}; echo '{marker}' $?"
168-
169-
# Send the combined command
170-
self.process.stdin.write(combined_command + "\n")
175+
self.process.stdin.write(command + "\n")
171176
self.process.stdin.flush()
177+
# Send exit code capture on a new line after user command completes
178+
self.process.stdin.write(f"echo '{marker}' $?\n")
179+
self.process.stdin.flush()
180+
172181
logger.debug("➡️ Sent command: %s", command)
182+
logger.debug("🔖 Waiting for marker: %s", marker)
173183

174184
# Collect output until marker is found or timeout
175185
output_lines = []
@@ -182,6 +192,7 @@ def send_command(
182192
try:
183193
# Check for output with a small timeout
184194
stream_type, line = self.output_queue.get(timeout=0.1)
195+
logger.debug("⬅️ Received line from %s: %s", stream_type, line)
185196

186197
# Check if this is our completion marker
187198
if marker in line:

src/microbots/tools/tool_definitions/browser-use/browser.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ async def main(args: list[str]) -> int:
2828
agent = Agent(
2929
task=what_to_browse,
3030
browser=browser,
31-
llm=ChatAzureOpenAI(model="gpt-4.1"),
31+
llm=ChatAzureOpenAI(model="gpt-5",temperature=1.0), # TODO: Gather it from environmental variable instead of hard coding.
3232
use_vision=False,
3333
)
3434
history: AgentHistoryList = await agent.run()

test/bot/browsing_bot_test.py

Lines changed: 0 additions & 29 deletions
This file was deleted.

test/bot/calculator/log_analysis_test.py

Lines changed: 0 additions & 30 deletions
This file was deleted.

0 commit comments

Comments
 (0)