Skip to content

Commit adf72bb

Browse files
simathihCopilot
andauthored
Add Python UTs (#2186)
* Fix duplicate config filenames and add unit tests with CI - Fix telegraf config filename collision: include plugin name in filename to prevent plugins in the same omiclass from overwriting each other (e.g. mem/swap/kernel_vmstat under 'memory') - Add comprehensive unit tests for all LAD-AMA-Common modules (91 tests) - Add GitHub Actions CI workflow triggered on LAD-AMA-Common changes Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Add unit tests for AzureMonitorAgent with CI and packaging exclusion - Add 104 unit tests covering agent.py, error handling, helpers, OS checks, and logrotate validation - Add GitHub Actions CI workflow for AzureMonitorAgent - Exclude tests/ directory from packaging zip in packaging.sh Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent b982fac commit adf72bb

8 files changed

Lines changed: 710 additions & 1 deletion

File tree

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
name: AzureMonitorAgent Tests
2+
3+
on:
4+
push:
5+
branches: [master, main]
6+
paths:
7+
- 'AzureMonitorAgent/**'
8+
- 'LAD-AMA-Common/**'
9+
pull_request:
10+
branches: [master, main]
11+
paths:
12+
- 'AzureMonitorAgent/**'
13+
- 'LAD-AMA-Common/**'
14+
15+
jobs:
16+
test:
17+
runs-on: ubuntu-latest
18+
strategy:
19+
matrix:
20+
python-version: ['3.9', '3.10', '3.11', '3.12']
21+
22+
steps:
23+
- uses: actions/checkout@v4
24+
25+
- name: Set up Python ${{ matrix.python-version }}
26+
uses: actions/setup-python@v5
27+
with:
28+
python-version: ${{ matrix.python-version }}
29+
30+
- name: Run AzureMonitorAgent tests
31+
working-directory: AzureMonitorAgent
32+
env:
33+
PYTHONPATH: ${{ github.workspace }}/LAD-AMA-Common
34+
run: python -m unittest discover -s tests -v

AzureMonitorAgent/packaging.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ fi
9696

9797
echo "Packaging extension $PACKAGE_NAME to $output_path"
9898
excluded_files="agent.version packaging.sh apply_version.sh update_version.sh"
99-
zip -r $output_path/$PACKAGE_NAME * -x $excluded_files "./test/*" "./extension-test/*" "./references" "./tmp"
99+
zip -r $output_path/$PACKAGE_NAME * -x $excluded_files "./test/*" "./tests/*" "./extension-test/*" "./references" "./tmp"
100100

101101
# validate package size is within limits; these limits come from arc, ideally they are removed in the future
102102
max_uncompressed_size=$((1000 * 1024 * 1024))

AzureMonitorAgent/tests/__init__.py

Whitespace-only changes.
Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
#!/usr/bin/env python
2+
"""
3+
Unit tests for AzureMonitorAgent/agent.py - pure logic functions only.
4+
"""
5+
6+
import sys
7+
import os
8+
import re
9+
import unittest
10+
from unittest.mock import patch, MagicMock
11+
12+
# Mock Linux-only modules before importing
13+
for mod_name in ('grp', 'pwd'):
14+
if mod_name not in sys.modules:
15+
sys.modules[mod_name] = MagicMock()
16+
17+
# Mock waagent/HUtil imports that agent.py tries to load at module level
18+
sys.modules['Utils'] = MagicMock()
19+
sys.modules['Utils.WAAgentUtil'] = MagicMock()
20+
sys.modules['Utils.WAAgentUtil'].waagent = MagicMock()
21+
sys.modules['Utils.HandlerUtil'] = MagicMock()
22+
23+
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
24+
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', 'LAD-AMA-Common')))
25+
26+
import agent
27+
28+
29+
class TestIsTruthy(unittest.TestCase):
30+
"""Tests for is_truthy function."""
31+
32+
def test_true_bool(self):
33+
self.assertTrue(agent.is_truthy(True))
34+
35+
def test_false_bool(self):
36+
self.assertFalse(agent.is_truthy(False))
37+
38+
def test_true_string(self):
39+
self.assertTrue(agent.is_truthy("true"))
40+
41+
def test_true_string_capitalized(self):
42+
self.assertTrue(agent.is_truthy("True"))
43+
44+
def test_true_string_uppercase(self):
45+
self.assertTrue(agent.is_truthy("TRUE"))
46+
47+
def test_true_string_with_whitespace(self):
48+
self.assertTrue(agent.is_truthy(" true "))
49+
50+
def test_false_string(self):
51+
self.assertFalse(agent.is_truthy("false"))
52+
53+
def test_none(self):
54+
self.assertFalse(agent.is_truthy(None))
55+
56+
def test_empty_string(self):
57+
self.assertFalse(agent.is_truthy(""))
58+
59+
def test_zero(self):
60+
self.assertFalse(agent.is_truthy(0))
61+
62+
def test_one(self):
63+
self.assertFalse(agent.is_truthy(1))
64+
65+
66+
class TestIsDpkgOrRpmLocked(unittest.TestCase):
67+
"""Tests for is_dpkg_or_rpm_locked."""
68+
69+
def test_dpkg_locked(self):
70+
output = "E: Could not get lock /var/lib/dpkg/lock - open"
71+
self.assertTrue(agent.is_dpkg_or_rpm_locked(1, output))
72+
73+
def test_rpm_locked(self):
74+
output = "error: waiting for transaction rpm lock on /var/lib/rpm/.rpm.lock"
75+
self.assertTrue(agent.is_dpkg_or_rpm_locked(1, output))
76+
77+
def test_not_locked_success(self):
78+
self.assertFalse(agent.is_dpkg_or_rpm_locked(0, "Success"))
79+
80+
def test_not_locked_other_error(self):
81+
self.assertFalse(agent.is_dpkg_or_rpm_locked(1, "Package not found"))
82+
83+
84+
class TestRetryIfDpkgOrRpmLocked(unittest.TestCase):
85+
"""Tests for retry_if_dpkg_or_rpm_locked."""
86+
87+
def test_locked_returns_retry(self):
88+
result = agent.retry_if_dpkg_or_rpm_locked(
89+
1, "dpkg status database is locked by another process"
90+
)
91+
self.assertTrue(result[0])
92+
93+
def test_not_locked_no_retry(self):
94+
result = agent.retry_if_dpkg_or_rpm_locked(0, "ok")
95+
self.assertFalse(result[0])
96+
97+
98+
class TestFinalCheckIfDpkgOrRpmLocked(unittest.TestCase):
99+
"""Tests for final_check_if_dpkg_or_rpm_locked."""
100+
101+
def test_locked_returns_special_code(self):
102+
code = agent.final_check_if_dpkg_or_rpm_locked(
103+
1, "dpkg lock held"
104+
)
105+
self.assertEqual(code, agent.DPKGOrRPMLockedErrorCode)
106+
107+
def test_not_locked_returns_original_code(self):
108+
code = agent.final_check_if_dpkg_or_rpm_locked(0, "ok")
109+
self.assertEqual(code, 0)
110+
111+
112+
class TestValidatePortNumber(unittest.TestCase):
113+
"""Tests for validate_port_number."""
114+
115+
@patch('agent.hutil_log_error')
116+
def test_valid_port(self, mock_log):
117+
self.assertEqual(agent.validate_port_number("8080", "test"), "8080")
118+
119+
@patch('agent.hutil_log_error')
120+
def test_valid_port_with_whitespace(self, mock_log):
121+
self.assertEqual(agent.validate_port_number(" 443 ", "test"), "443")
122+
123+
@patch('agent.hutil_log_error')
124+
def test_port_min(self, mock_log):
125+
self.assertEqual(agent.validate_port_number("1", "test"), "1")
126+
127+
@patch('agent.hutil_log_error')
128+
def test_port_max(self, mock_log):
129+
self.assertEqual(agent.validate_port_number("65535", "test"), "65535")
130+
131+
@patch('agent.hutil_log_error')
132+
def test_port_zero_invalid(self, mock_log):
133+
self.assertEqual(agent.validate_port_number("0", "test"), "")
134+
135+
@patch('agent.hutil_log_error')
136+
def test_port_too_high(self, mock_log):
137+
self.assertEqual(agent.validate_port_number("65536", "test"), "")
138+
139+
@patch('agent.hutil_log_error')
140+
def test_port_negative(self, mock_log):
141+
self.assertEqual(agent.validate_port_number("-1", "test"), "")
142+
143+
@patch('agent.hutil_log_error')
144+
def test_port_non_numeric(self, mock_log):
145+
self.assertEqual(agent.validate_port_number("abc", "test"), "")
146+
147+
@patch('agent.hutil_log_error')
148+
def test_port_empty(self, mock_log):
149+
self.assertEqual(agent.validate_port_number("", "test"), "")
150+
151+
@patch('agent.hutil_log_error')
152+
def test_port_none(self, mock_log):
153+
self.assertEqual(agent.validate_port_number(None, "test"), "")
154+
155+
156+
class TestGetProxyMode(unittest.TestCase):
157+
"""Tests for get_proxy_mode."""
158+
159+
def test_none_settings(self):
160+
self.assertIsNone(agent.get_proxy_mode(None))
161+
162+
def test_no_proxy_key(self):
163+
self.assertIsNone(agent.get_proxy_mode({"other": "value"}))
164+
165+
def test_proxy_no_mode(self):
166+
self.assertIsNone(agent.get_proxy_mode({"proxy": {}}))
167+
168+
def test_proxy_with_mode(self):
169+
self.assertEqual(
170+
agent.get_proxy_mode({"proxy": {"mode": "application"}}),
171+
"application"
172+
)
173+
174+
def test_proxy_none_value(self):
175+
self.assertIsNone(agent.get_proxy_mode({"proxy": None}))
176+
177+
178+
class TestGetServiceCommand(unittest.TestCase):
179+
"""Tests for get_service_command."""
180+
181+
@patch('agent.is_systemd', return_value=True)
182+
def test_systemd_single_op(self, _):
183+
cmd = agent.get_service_command("myservice", "start")
184+
self.assertEqual(cmd, "systemctl start myservice")
185+
186+
@patch('agent.is_systemd', return_value=True)
187+
def test_systemd_multiple_ops(self, _):
188+
cmd = agent.get_service_command("myservice", "daemon-reload", "restart")
189+
self.assertEqual(cmd, "systemctl daemon-reload myservice && systemctl restart myservice")
190+
191+
@patch('agent.is_systemd', return_value=False)
192+
@patch('agent.hutil_log_info')
193+
def test_initd_fallback(self, _, __):
194+
cmd = agent.get_service_command("myservice", "start")
195+
self.assertEqual(cmd, "/etc/init.d/myservice start")
196+
197+
198+
class TestConstants(unittest.TestCase):
199+
"""Tests for agent constants."""
200+
201+
def test_error_codes(self):
202+
self.assertEqual(agent.GenericErrorCode, 1)
203+
self.assertEqual(agent.UnsupportedOperatingSystem, 51)
204+
self.assertEqual(agent.MissingorInvalidParameterErrorCode, 53)
205+
self.assertEqual(agent.DPKGOrRPMLockedErrorCode, 56)
206+
self.assertEqual(agent.MissingDependency, 52)
207+
208+
def test_supported_arch(self):
209+
self.assertIn('x86_64', agent.SupportedArch)
210+
self.assertIn('aarch64', agent.SupportedArch)
211+
212+
def test_config_keys(self):
213+
self.assertEqual(agent.GenevaConfigKey, "genevaConfiguration")
214+
self.assertEqual(agent.AzureMonitorConfigKey, "azureMonitorConfiguration")
215+
216+
217+
if __name__ == '__main__':
218+
unittest.main()
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
#!/usr/bin/env python
2+
"""
3+
Unit tests for AzureMonitorAgent/ama_tst/modules/high_cpu_mem/check_logrot.py
4+
"""
5+
6+
import sys
7+
import os
8+
import unittest
9+
from unittest.mock import patch
10+
11+
# Add path for ama_tst modules
12+
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'ama_tst', 'modules')))
13+
14+
import error_codes
15+
from errors import error_info
16+
from high_cpu_mem.check_logrot import hr2bytes, check_size_config
17+
18+
19+
class TestHr2Bytes(unittest.TestCase):
20+
"""Tests for hr2bytes function."""
21+
22+
def test_plain_digits(self):
23+
self.assertEqual(hr2bytes("1024"), 1024)
24+
25+
def test_kilobytes(self):
26+
self.assertEqual(hr2bytes("5k"), 5000)
27+
28+
def test_megabytes(self):
29+
self.assertEqual(hr2bytes("10M"), 10000000)
30+
31+
def test_gigabytes(self):
32+
self.assertEqual(hr2bytes("2G"), 2000000000)
33+
34+
def test_invalid_unit(self):
35+
self.assertIsNone(hr2bytes("5X"))
36+
37+
def test_invalid_format(self):
38+
self.assertIsNone(hr2bytes("abc"))
39+
40+
def test_zero_kilobytes(self):
41+
self.assertEqual(hr2bytes("0k"), 0)
42+
43+
def test_one_megabyte(self):
44+
self.assertEqual(hr2bytes("1M"), 1000000)
45+
46+
47+
class TestCheckSizeConfig(unittest.TestCase):
48+
"""Tests for check_size_config function."""
49+
50+
def setUp(self):
51+
error_info.clear()
52+
53+
def test_no_size_config(self):
54+
configs = {"/var/log/test.log": {"rotate 5", "daily"}}
55+
result = check_size_config(configs)
56+
self.assertEqual(result, error_codes.NO_ERROR)
57+
58+
def test_invalid_size_format(self):
59+
configs = {"/var/log/test.log": {"size XYZ", "rotate 5"}}
60+
result = check_size_config(configs)
61+
self.assertEqual(result, error_codes.ERR_LOGROTATE_SIZE)
62+
63+
@patch('os.path.getsize', return_value=500)
64+
def test_file_under_limit(self, mock_size):
65+
configs = {"/var/log/test.log": {"size 1M", "rotate 5"}}
66+
result = check_size_config(configs)
67+
self.assertEqual(result, error_codes.NO_ERROR)
68+
69+
@patch('os.path.getsize', return_value=2000000)
70+
def test_file_over_limit(self, mock_size):
71+
configs = {"/var/log/test.log": {"size 1M", "rotate 5"}}
72+
result = check_size_config(configs)
73+
self.assertEqual(result, error_codes.WARN_LOGROTATE)
74+
75+
@patch('os.path.getsize', side_effect=OSError(13, "Permission denied"))
76+
def test_permission_denied(self, mock_size):
77+
import errno
78+
err = OSError(errno.EACCES, "Permission denied")
79+
with patch('os.path.getsize', side_effect=err):
80+
configs = {"/var/log/test.log": {"size 1M", "rotate 5"}}
81+
result = check_size_config(configs)
82+
self.assertEqual(result, error_codes.ERR_SUDO_PERMS)
83+
84+
@patch('os.path.getsize')
85+
def test_file_not_found_missingok(self, mock_size):
86+
import errno
87+
mock_size.side_effect = OSError(errno.ENOENT, "No such file")
88+
configs = {"/var/log/test.log": {"size 1M", "missingok"}}
89+
result = check_size_config(configs)
90+
self.assertEqual(result, error_codes.NO_ERROR)
91+
92+
@patch('os.path.getsize')
93+
def test_file_not_found_no_missingok(self, mock_size):
94+
import errno
95+
mock_size.side_effect = OSError(errno.ENOENT, "No such file")
96+
configs = {"/var/log/test.log": {"size 1M", "rotate 5"}}
97+
result = check_size_config(configs)
98+
self.assertEqual(result, error_codes.ERR_FILE_MISSING)
99+
100+
101+
if __name__ == '__main__':
102+
unittest.main()

0 commit comments

Comments
 (0)