#!/usr/bin/env python # # Glances - An eye on your system # # SPDX-FileCopyrightText: 2024 Nicolas Hennion # # SPDX-License-Identifier: LGPL-3.0-only # """Tests for the JSON serializer module. These tests are designed to be runnable standalone without requiring the full Glances initialization, for faster CI feedback. """ import json import os import sys import types import unittest from datetime import datetime from unittest.mock import Mock def setup_mock_modules(): """Set up mock modules to allow importing the serializer without full Glances.""" # Create mock glances package if 'glances' not in sys.modules: glances_pkg = types.ModuleType('glances') glances_pkg.__path__ = [os.path.join(os.path.dirname(__file__), '..', 'glances')] sys.modules['glances'] = glances_pkg # Create mock glances.outputs package if 'glances.outputs' not in sys.modules: glances_outputs = types.ModuleType('glances.outputs') sys.modules['glances.outputs'] = glances_outputs # Create mock globals if 'glances.globals' not in sys.modules: glances_globals = types.ModuleType('glances.globals') glances_globals.json_dumps = lambda data: json.dumps(data, default=str).encode('utf-8') glances_globals.json_loads = lambda data: json.loads(data.decode('utf-8') if isinstance(data, bytes) else data) sys.modules['glances.globals'] = glances_globals # Create mock logger if 'glances.logger' not in sys.modules: class MockLogger: def debug(self, msg): pass def error(self, msg): pass def info(self, msg): pass glances_logger = types.ModuleType('glances.logger') glances_logger.logger = MockLogger() sys.modules['glances.logger'] = glances_logger # Setup mocks BEFORE any other imports setup_mock_modules() # Now we can import our serializer # Direct exec to avoid import conflicts serializer_path = os.path.join(os.path.dirname(__file__), '..', 'glances', 'outputs', 'glances_json_serializer.py') with open(serializer_path) as f: code = f.read() exec(compile(code, serializer_path, 'exec'), sys.modules['glances.outputs'].__dict__) PluginSerializationError = sys.modules['glances.outputs'].PluginSerializationError GlancesJSONSerializer = sys.modules['glances.outputs'].GlancesJSONSerializer class TestPluginSerializationError(unittest.TestCase): """Test the PluginSerializationError class.""" def test_error_to_dict(self): error = PluginSerializationError("cpu", "Test error") result = error.to_dict() self.assertEqual(result["error"], True) self.assertEqual(result["plugin"], "cpu") self.assertEqual(result["message"], "Test error") class TestGlancesJSONSerializer(unittest.TestCase): """Test the GlancesJSONSerializer class.""" def setUp(self): self.serializer = GlancesJSONSerializer() def test_normalize_none(self): self.assertIsNone(self.serializer.normalize_value(None)) def test_normalize_bytes(self): result = self.serializer.normalize_value(b'hello') self.assertEqual(result, 'hello') def test_normalize_datetime(self): dt = datetime(2024, 1, 15, 10, 30, 0) result = self.serializer.normalize_value(dt) self.assertEqual(result, '2024-01-15T10:30:00') def test_normalize_primitives(self): self.assertEqual(self.serializer.normalize_value(42), 42) self.assertEqual(self.serializer.normalize_value(3.14), 3.14) self.assertEqual(self.serializer.normalize_value('test'), 'test') self.assertEqual(self.serializer.normalize_value(True), True) def test_normalize_dict(self): data = {'a': 1, 'b': b'bytes', 'c': None} result = self.serializer.normalize_value(data) self.assertEqual(result['a'], 1) self.assertEqual(result['b'], 'bytes') self.assertIsNone(result['c']) def test_normalize_list(self): data = [1, b'bytes', 'string', None] result = self.serializer.normalize_value(data) self.assertEqual(result, [1, 'bytes', 'string', None]) def test_normalize_nested(self): data = {'outer': {'inner': [1, 2, {'deep': b'value'}]}} result = self.serializer.normalize_value(data) self.assertEqual(result['outer']['inner'][2]['deep'], 'value') def test_serialize_plugin_data_none(self): result = self.serializer.serialize_plugin_data('test', None) self.assertIsNone(result) def test_serialize_plugin_data_bytes_json(self): data = b'{"cpu": 50, "memory": 75}' result = self.serializer.serialize_plugin_data('test', data) self.assertEqual(result['cpu'], 50) self.assertEqual(result['memory'], 75) def test_serialize_plugin_data_bytes_invalid_json(self): data = b'not valid json' result = self.serializer.serialize_plugin_data('test', data) self.assertEqual(result, 'not valid json') def test_serialize_plugin_data_dict(self): data = {'cpu': 50, 'memory': 75} result = self.serializer.serialize_plugin_data('test', data) self.assertEqual(result['cpu'], 50) self.assertEqual(result['memory'], 75) def test_to_json_string_dict(self): data = {'key': 'value', 'number': 42} result = self.serializer.to_json_string(data) parsed = json.loads(result) self.assertEqual(parsed['key'], 'value') self.assertEqual(parsed['number'], 42) def test_to_json_string_list(self): data = [1, 2, 3, 'four'] result = self.serializer.to_json_string(data) parsed = json.loads(result) self.assertEqual(parsed, [1, 2, 3, 'four']) def test_serialize_plugins_empty_list(self): mock_stats = Mock() mock_stats.getPluginsList.return_value = [] result = self.serializer.serialize_plugins(mock_stats, []) self.assertEqual(result, {}) def test_serialize_plugins_with_data(self): mock_plugin = Mock() mock_plugin.is_enabled.return_value = True mock_plugin.get_json.return_value = b'{"value": 42}' mock_stats = Mock() mock_stats.getPluginsList.return_value = ['test_plugin'] mock_stats.get_plugin.return_value = mock_plugin result = self.serializer.serialize_plugins(mock_stats, ['test_plugin']) self.assertIn('test_plugin', result) self.assertEqual(result['test_plugin']['value'], 42) def test_serialize_plugins_plugin_not_enabled(self): mock_plugin = Mock() mock_plugin.is_enabled.return_value = False mock_stats = Mock() mock_stats.getPluginsList.return_value = ['disabled_plugin'] mock_stats.get_plugin.return_value = mock_plugin result = self.serializer.serialize_plugins(mock_stats, ['disabled_plugin']) self.assertNotIn('disabled_plugin', result) def test_serialize_plugins_plugin_not_found(self): mock_stats = Mock() mock_stats.getPluginsList.return_value = [] mock_stats.get_plugin.return_value = None result = self.serializer.serialize_plugins(mock_stats, ['nonexistent']) self.assertNotIn('nonexistent', result) def test_serialize_plugins_with_metadata(self): serializer = GlancesJSONSerializer(include_metadata=True) mock_stats = Mock() mock_stats.getPluginsList.return_value = [] result = serializer.serialize_plugins(mock_stats, []) self.assertIn('_metadata', result) self.assertIn('timestamp', result['_metadata']) self.assertIn('plugin_count', result['_metadata']) def test_serialize_to_string_produces_valid_json(self): mock_plugin = Mock() mock_plugin.is_enabled.return_value = True mock_plugin.get_json.return_value = b'{"cpu": 25.5}' mock_stats = Mock() mock_stats.getPluginsList.return_value = ['cpu'] mock_stats.get_plugin.return_value = mock_plugin result = self.serializer.serialize_to_string(mock_stats, ['cpu']) parsed = json.loads(result) self.assertIn('cpu', parsed) def test_serialize_handles_unicode(self): data = {'name': 'café ☕', 'value': 42} result = self.serializer.serialize_plugin_data('test', data) self.assertEqual(result['name'], 'café ☕') class TestSerializerEdgeCases(unittest.TestCase): """Test edge cases and error handling.""" def test_serializer_with_errors_disabled(self): serializer = GlancesJSONSerializer(include_errors=False) mock_plugin = Mock() mock_plugin.is_enabled.return_value = True mock_plugin.get_json.side_effect = Exception("Plugin error") mock_stats = Mock() mock_stats.getPluginsList.return_value = ['failing_plugin'] mock_stats.get_plugin.return_value = mock_plugin result = serializer.serialize_plugins(mock_stats, ['failing_plugin']) self.assertNotIn('_errors', result) self.assertNotIn('failing_plugin', result) def test_serializer_with_errors_enabled(self): serializer = GlancesJSONSerializer(include_errors=True) mock_plugin = Mock() mock_plugin.is_enabled.return_value = True mock_plugin.get_json.side_effect = Exception("Plugin error") mock_stats = Mock() mock_stats.getPluginsList.return_value = ['failing_plugin'] mock_stats.get_plugin.return_value = mock_plugin result = serializer.serialize_plugins(mock_stats, ['failing_plugin']) self.assertIn('_errors', result) def test_multiple_plugins_one_fails(self): """Verify that one plugin failing doesn't break the entire output.""" serializer = GlancesJSONSerializer(include_errors=True) good_plugin = Mock() good_plugin.is_enabled.return_value = True good_plugin.get_json.return_value = b'{"status": "ok"}' bad_plugin = Mock() bad_plugin.is_enabled.return_value = True bad_plugin.get_json.side_effect = Exception("Failed") mock_stats = Mock() mock_stats.getPluginsList.return_value = ['good', 'bad'] def get_plugin(name): return good_plugin if name == 'good' else bad_plugin mock_stats.get_plugin.side_effect = get_plugin result = serializer.serialize_plugins(mock_stats, ['good', 'bad']) self.assertIn('good', result) self.assertEqual(result['good']['status'], 'ok') self.assertIn('_errors', result) def test_empty_bytes_input(self): serializer = GlancesJSONSerializer() result = serializer.serialize_plugin_data('test', b'') self.assertEqual(result, '') def test_all_plugins_fail_still_produces_valid_json(self): """Verify that even total failure produces parseable JSON.""" serializer = GlancesJSONSerializer(include_errors=True) failing_plugin = Mock() failing_plugin.is_enabled.return_value = True failing_plugin.get_json.side_effect = Exception("Total failure") mock_stats = Mock() mock_stats.getPluginsList.return_value = ['fail1', 'fail2'] mock_stats.get_plugin.return_value = failing_plugin result = serializer.serialize_to_string(mock_stats, ['fail1', 'fail2']) parsed = json.loads(result) # Should not raise self.assertIn('_errors', parsed) def test_empty_plugins_produces_valid_json(self): """Verify that empty plugin list produces valid JSON.""" serializer = GlancesJSONSerializer() mock_stats = Mock() mock_stats.getPluginsList.return_value = [] result = serializer.serialize_to_string(mock_stats, []) parsed = json.loads(result) self.assertEqual(parsed, {}) def test_output_structure_consistency(self): """Verify the output structure is consistent.""" serializer = GlancesJSONSerializer(include_errors=True, include_metadata=True) mock_plugin = Mock() mock_plugin.is_enabled.return_value = True mock_plugin.get_json.return_value = b'{"value": 100}' mock_stats = Mock() mock_stats.getPluginsList.return_value = ['cpu'] mock_stats.get_plugin.return_value = mock_plugin result = serializer.serialize_plugins(mock_stats, ['cpu']) # Verify structure self.assertIsInstance(result, dict) self.assertIn('cpu', result) self.assertIn('_metadata', result) self.assertIsInstance(result['_metadata'], dict) self.assertIn('timestamp', result['_metadata']) self.assertIn('plugin_count', result['_metadata']) if __name__ == '__main__': unittest.main()