A Python port of the Invisible Internet Project (I2P)
at main 531 lines 17 kB view raw
1"""Tests for i2p_util core module — logging, hex, system, cache, collections, random, siphash.""" 2 3import struct 4import threading 5 6import pytest 7 8 9# === Log / LogManager === 10 11class TestLog: 12 def test_level_constants(self): 13 from i2p_util.log import Log 14 assert Log.DEBUG < Log.INFO < Log.WARN < Log.ERROR < Log.CRIT 15 16 def test_get_level(self): 17 from i2p_util.log import Log 18 assert Log.get_level("DEBUG") == Log.DEBUG 19 assert Log.get_level("info") == Log.INFO 20 assert Log.get_level("WARN") == Log.WARN 21 assert Log.get_level("ERROR") == Log.ERROR 22 assert Log.get_level("CRIT") == Log.CRIT 23 assert Log.get_level("CRITICAL") == Log.CRIT 24 # Unknown defaults to DEBUG 25 assert Log.get_level("bogus") == Log.DEBUG 26 27 def test_to_level_string(self): 28 from i2p_util.log import Log 29 assert Log.to_level_string(Log.DEBUG) == "DEBUG" 30 assert Log.to_level_string(Log.INFO) == "INFO" 31 assert Log.to_level_string(Log.WARN) == "WARN" 32 assert Log.to_level_string(Log.ERROR) == "ERROR" 33 assert Log.to_level_string(Log.CRIT) == "CRIT" 34 assert Log.to_level_string(999) == "DEBUG" 35 36 def test_should_log_respects_priority(self): 37 from i2p_util.log import Log, LogManager 38 mgr = LogManager() 39 log = mgr.get_log("test.priority") 40 log.set_minimum_priority(Log.WARN) 41 assert not log.should_log(Log.DEBUG) 42 assert not log.should_log(Log.INFO) 43 assert log.should_log(Log.WARN) 44 assert log.should_log(Log.ERROR) 45 assert log.should_log(Log.CRIT) 46 47 def test_should_debug_info_warn_error(self): 48 from i2p_util.log import Log, LogManager 49 mgr = LogManager() 50 log = mgr.get_log("test.shorthand") 51 log.set_minimum_priority(Log.ERROR) 52 assert not log.should_debug() 53 assert not log.should_info() 54 assert not log.should_warn() 55 assert log.should_error() 56 57 def test_log_does_not_raise(self): 58 from i2p_util.log import Log, LogManager 59 mgr = LogManager() 60 log = mgr.get_log("test.noexc") 61 log.set_minimum_priority(Log.DEBUG) 62 # Should not raise even with exception 63 log.debug("msg") 64 log.info("msg") 65 log.warn("msg") 66 log.error("msg") 67 log.log(Log.CRIT, "critical msg") 68 log.log(Log.ERROR, "with exc", ValueError("boom")) 69 log.log_always(Log.DEBUG, "forced") 70 71 def test_log_name(self): 72 from i2p_util.log import LogManager 73 mgr = LogManager() 74 log = mgr.get_log("my.logger") 75 assert log.name == "my.logger" 76 77 def test_log_from_class(self): 78 from i2p_util.log import LogManager 79 mgr = LogManager() 80 log = mgr.get_log(str) 81 assert "str" in log.name 82 83 84class TestLogManager: 85 def test_get_log_returns_same_instance(self): 86 from i2p_util.log import LogManager 87 mgr = LogManager() 88 a = mgr.get_log("same") 89 b = mgr.get_log("same") 90 assert a is b 91 92 def test_get_logs(self): 93 from i2p_util.log import LogManager 94 mgr = LogManager() 95 mgr.get_log("a") 96 mgr.get_log("b") 97 assert len(mgr.get_logs()) >= 2 98 99 def test_set_default_limit(self): 100 from i2p_util.log import Log, LogManager 101 mgr = LogManager() 102 log = mgr.get_log("default.test") 103 mgr.set_default_limit(Log.ERROR) 104 assert log.get_minimum_priority() == Log.ERROR 105 assert mgr.get_default_limit() == Log.ERROR 106 107 def test_shutdown_clears(self): 108 from i2p_util.log import LogManager 109 mgr = LogManager() 110 mgr.get_log("x") 111 mgr.shutdown() 112 assert len(mgr.get_logs()) == 0 113 114 115# === HexDump === 116 117class TestHexDump: 118 def test_to_hex(self): 119 from i2p_util.hex import HexDump 120 assert HexDump.to_hex(b"\xde\xad\xbe\xef") == "deadbeef" 121 122 def test_from_hex(self): 123 from i2p_util.hex import HexDump 124 assert HexDump.from_hex("deadbeef") == b"\xde\xad\xbe\xef" 125 126 def test_roundtrip(self): 127 from i2p_util.hex import HexDump 128 data = b"Hello, I2P!" 129 assert HexDump.from_hex(HexDump.to_hex(data)) == data 130 131 def test_dump_format(self): 132 from i2p_util.hex import HexDump 133 data = b"ABCD" 134 out = HexDump.dump(data) 135 assert "00000000" in out 136 assert "41 42 43 44" in out 137 assert "|ABCD|" in out 138 139 def test_dump_non_printable(self): 140 from i2p_util.hex import HexDump 141 data = bytes(range(32)) 142 out = HexDump.dump(data) 143 # Non-printable chars shown as dots 144 assert "." in out 145 146 def test_dump_offset_length(self): 147 from i2p_util.hex import HexDump 148 data = b"0123456789" 149 out = HexDump.dump(data, offset=2, length=4) 150 # Should show "2345" 151 assert "32 33 34 35" in out 152 153 def test_empty(self): 154 from i2p_util.hex import HexDump 155 assert HexDump.to_hex(b"") == "" 156 assert HexDump.from_hex("") == b"" 157 assert HexDump.dump(b"") == "" 158 159 160# === SystemVersion === 161 162class TestSystemVersion: 163 def test_get_os_returns_string(self): 164 from i2p_util.system import SystemVersion 165 os_name = SystemVersion.get_os() 166 assert isinstance(os_name, str) 167 assert len(os_name) > 0 168 169 def test_get_arch_returns_string(self): 170 from i2p_util.system import SystemVersion 171 arch = SystemVersion.get_arch() 172 assert arch in ("amd64", "arm64", "arm", "386", "unknown") 173 174 def test_is_64bit(self): 175 from i2p_util.system import SystemVersion 176 import struct 177 bits = struct.calcsize("P") * 8 178 assert SystemVersion.is_64bit() == (bits == 64) 179 180 def test_get_cores_positive(self): 181 from i2p_util.system import SystemVersion 182 assert SystemVersion.get_cores() >= 1 183 184 def test_get_max_memory_positive(self): 185 from i2p_util.system import SystemVersion 186 assert SystemVersion.get_max_memory() > 0 187 188 def test_is_android_false(self): 189 from i2p_util.system import SystemVersion 190 assert SystemVersion.is_android() is False 191 192 def test_python_version(self): 193 from i2p_util.system import SystemVersion 194 ver = SystemVersion.python_version() 195 assert "." in ver 196 197 def test_platform_booleans_consistent(self): 198 from i2p_util.system import SystemVersion 199 os_name = SystemVersion.get_os() 200 if os_name == "linux": 201 assert SystemVersion.is_linux() 202 elif os_name == "mac": 203 assert SystemVersion.is_mac() 204 elif os_name == "windows": 205 assert SystemVersion.is_windows() 206 207 def test_is_arm(self): 208 from i2p_util.system import SystemVersion 209 result = SystemVersion.is_arm() 210 assert isinstance(result, bool) 211 212 def test_is_x86(self): 213 from i2p_util.system import SystemVersion 214 result = SystemVersion.is_x86() 215 assert isinstance(result, bool) 216 217 def test_is_slow(self): 218 from i2p_util.system import SystemVersion 219 result = SystemVersion.is_slow() 220 assert isinstance(result, bool) 221 222 def test_get_os_returns_known(self): 223 from i2p_util.system import SystemVersion 224 from unittest.mock import patch 225 with patch("platform.system", return_value="Darwin"): 226 assert SystemVersion.get_os() == "mac" 227 with patch("platform.system", return_value="Windows"): 228 assert SystemVersion.get_os() == "windows" 229 with patch("platform.system", return_value="Linux"): 230 assert SystemVersion.get_os() == "linux" 231 with patch("platform.system", return_value="FreeBSD"): 232 assert SystemVersion.get_os() == "freebsd" 233 234 def test_get_arch_variants(self): 235 from i2p_util.system import SystemVersion 236 from unittest.mock import patch 237 with patch("platform.machine", return_value="x86_64"): 238 assert SystemVersion.get_arch() == "amd64" 239 with patch("platform.machine", return_value="aarch64"): 240 assert SystemVersion.get_arch() == "arm64" 241 with patch("platform.machine", return_value="armv7l"): 242 assert SystemVersion.get_arch() == "arm" 243 with patch("platform.machine", return_value="i686"): 244 assert SystemVersion.get_arch() == "386" 245 with patch("platform.machine", return_value="mips"): 246 assert SystemVersion.get_arch() == "unknown" 247 248 def test_is_windows(self): 249 from i2p_util.system import SystemVersion 250 from unittest.mock import patch 251 with patch("platform.system", return_value="Windows"): 252 assert SystemVersion.is_windows() is True 253 with patch("platform.system", return_value="Linux"): 254 assert SystemVersion.is_windows() is False 255 256 def test_is_mac(self): 257 from i2p_util.system import SystemVersion 258 from unittest.mock import patch 259 with patch("platform.system", return_value="Darwin"): 260 assert SystemVersion.is_mac() is True 261 with patch("platform.system", return_value="Linux"): 262 assert SystemVersion.is_mac() is False 263 264 def test_get_max_memory_fallback(self): 265 from i2p_util.system import SystemVersion 266 from unittest.mock import patch 267 with patch("resource.getrlimit", side_effect=ValueError("nope")): 268 result = SystemVersion.get_max_memory() 269 assert result == 2 * 1024 * 1024 * 1024 270 271 272# === LHMCache === 273 274class TestLHMCache: 275 def test_basic_set_get(self): 276 from i2p_util.cache import LHMCache 277 c = LHMCache(3) 278 c["a"] = 1 279 assert c["a"] == 1 280 281 def test_eviction(self): 282 from i2p_util.cache import LHMCache 283 c = LHMCache(2) 284 c["a"] = 1 285 c["b"] = 2 286 c["c"] = 3 # Should evict "a" 287 assert "a" not in c 288 assert c["b"] == 2 289 assert c["c"] == 3 290 291 def test_access_refreshes(self): 292 from i2p_util.cache import LHMCache 293 c = LHMCache(2) 294 c["a"] = 1 295 c["b"] = 2 296 _ = c["a"] # Access "a" — moves it to end 297 c["c"] = 3 # Should evict "b", not "a" 298 assert "a" in c 299 assert "b" not in c 300 301 def test_update_refreshes(self): 302 from i2p_util.cache import LHMCache 303 c = LHMCache(2) 304 c["a"] = 1 305 c["b"] = 2 306 c["a"] = 10 # Update "a" — moves it to end 307 c["c"] = 3 # Should evict "b" 308 assert c["a"] == 10 309 assert "b" not in c 310 311 def test_get_default(self): 312 from i2p_util.cache import LHMCache 313 c = LHMCache(3) 314 assert c.get("missing") is None 315 assert c.get("missing", 42) == 42 316 317 318# === SimpleByteCache === 319 320class TestSimpleByteCache: 321 def test_acquire_returns_bytearray(self): 322 from i2p_util.cache import SimpleByteCache 323 sbc = SimpleByteCache(4, 16) 324 buf = sbc.acquire() 325 assert isinstance(buf, bytearray) 326 assert len(buf) == 16 327 328 def test_release_and_reacquire(self): 329 from i2p_util.cache import SimpleByteCache 330 sbc = SimpleByteCache(4, 8) 331 buf = sbc.acquire() 332 buf[:] = b"\xff" * 8 333 sbc.release(buf) 334 # Re-acquired buffer should be zeroed 335 buf2 = sbc.acquire() 336 assert len(buf2) == 8 337 338 def test_pool_limit(self): 339 from i2p_util.cache import SimpleByteCache 340 sbc = SimpleByteCache(2, 4) 341 bufs = [sbc.acquire() for _ in range(5)] 342 for b in bufs: 343 sbc.release(b) 344 # Pool should only hold 2 345 assert len(sbc._pool) <= 2 346 347 def test_get_instance_singleton(self): 348 from i2p_util.cache import SimpleByteCache 349 # Clear class state first 350 a = SimpleByteCache.get_instance(64) 351 b = SimpleByteCache.get_instance(64) 352 assert a is b 353 354 355# === ByteCache === 356 357class TestByteCache: 358 def test_acquire_release(self): 359 from i2p_util.cache import ByteCache 360 bc = ByteCache.get_instance(4, 32) 361 buf = bc.acquire() 362 assert len(buf) == 32 363 bc.release(buf) 364 365 def test_clear_all(self): 366 from i2p_util.cache import ByteCache 367 bc = ByteCache.get_instance(4, 32) 368 buf = bc.acquire() 369 bc.release(buf) 370 ByteCache.clear_all() 371 372 373# === OrderedProperties === 374 375class TestOrderedProperties: 376 def test_sorted_keys(self): 377 from i2p_util.collections import OrderedProperties 378 p = OrderedProperties() 379 p["c"] = "3" 380 p["a"] = "1" 381 p["b"] = "2" 382 assert p.keys() == ["a", "b", "c"] 383 384 def test_sorted_items(self): 385 from i2p_util.collections import OrderedProperties 386 p = OrderedProperties() 387 p["z"] = "last" 388 p["a"] = "first" 389 items = p.items() 390 assert items[0] == ("a", "first") 391 assert items[1] == ("z", "last") 392 393 def test_get_set_property(self): 394 from i2p_util.collections import OrderedProperties 395 p = OrderedProperties() 396 p.set_property("key", "value") 397 assert p.get_property("key") == "value" 398 assert p.get_property("missing", "default") == "default" 399 400 def test_values_sorted_by_key(self): 401 from i2p_util.collections import OrderedProperties 402 p = OrderedProperties() 403 p["b"] = "B" 404 p["a"] = "A" 405 assert p.values() == ["A", "B"] 406 407 408# === RandomSource === 409 410class TestRandomSource: 411 def test_singleton(self): 412 from i2p_util.random import RandomSource 413 a = RandomSource.get_instance() 414 b = RandomSource.get_instance() 415 assert a is b 416 417 def test_next_int_range(self): 418 from i2p_util.random import RandomSource 419 rs = RandomSource.get_instance() 420 for _ in range(100): 421 v = rs.next_int(10) 422 assert 0 <= v < 10 423 424 def test_next_int_bound_error(self): 425 from i2p_util.random import RandomSource 426 rs = RandomSource.get_instance() 427 with pytest.raises(ValueError): 428 rs.next_int(0) 429 with pytest.raises(ValueError): 430 rs.next_int(-1) 431 432 def test_signed_next_int_range(self): 433 from i2p_util.random import RandomSource 434 rs = RandomSource.get_instance() 435 v = rs.signed_next_int() 436 assert -(2**31) <= v < 2**31 437 438 def test_next_long(self): 439 from i2p_util.random import RandomSource 440 rs = RandomSource.get_instance() 441 for _ in range(50): 442 v = rs.next_long(1000) 443 assert 0 <= v < 1000 444 445 def test_next_bytes_length(self): 446 from i2p_util.random import RandomSource 447 rs = RandomSource.get_instance() 448 b = rs.next_bytes(32) 449 assert len(b) == 32 450 451 def test_next_bytes_into(self): 452 from i2p_util.random import RandomSource 453 rs = RandomSource.get_instance() 454 buf = bytearray(16) 455 rs.next_bytes_into(buf, 4, 8) 456 # First 4 and last 4 should still be zero 457 assert buf[:4] == b"\x00" * 4 458 assert buf[12:] == b"\x00" * 4 459 # Middle 8 bytes should be filled (overwhelmingly unlikely to be all zeros) 460 461 def test_next_boolean(self): 462 from i2p_util.random import RandomSource 463 rs = RandomSource.get_instance() 464 results = {rs.next_boolean() for _ in range(100)} 465 assert True in results 466 assert False in results 467 468 469# === SipHash === 470 471class TestSipHash: 472 def test_digest_deterministic(self): 473 from i2p_util.siphash import SipHash 474 data = b"hello" 475 a = SipHash.digest(data) 476 b = SipHash.digest(data) 477 assert a == b 478 479 def test_digest_different_data(self): 480 from i2p_util.siphash import SipHash 481 a = SipHash.digest(b"hello") 482 b = SipHash.digest(b"world") 483 assert a != b 484 485 def test_digest_is_64bit(self): 486 from i2p_util.siphash import SipHash 487 h = SipHash.digest(b"test data") 488 assert 0 <= h < (1 << 64) 489 490 def test_hash_code_is_32bit(self): 491 from i2p_util.siphash import SipHash 492 h = SipHash.hash_code(b"test data") 493 assert 0 <= h < (1 << 32) 494 495 def test_hash_code_none(self): 496 from i2p_util.siphash import SipHash 497 assert SipHash.hash_code(None) == 0 498 499 def test_digest_with_offset_length(self): 500 from i2p_util.siphash import SipHash 501 data = b"XXXhelloXXX" 502 a = SipHash.digest(data, 3, 5) 503 b = SipHash.digest(b"hello") 504 assert a == b 505 506 def test_empty_data(self): 507 from i2p_util.siphash import SipHash 508 h = SipHash.digest(b"") 509 assert isinstance(h, int) 510 511 def test_various_lengths(self): 512 """Test all remainder cases (0-7 trailing bytes).""" 513 from i2p_util.siphash import SipHash 514 for length in range(17): 515 data = bytes(range(length)) 516 h = SipHash.digest(data) 517 assert 0 <= h < (1 << 64) 518 519 def test_known_siphash24_vectors(self): 520 """Test against known SipHash-2-4 test vectors with k0=k1=0.""" 521 from i2p_util.siphash import SipHash 522 # With known keys, we can verify the algorithm is correct 523 # Use internal method with k0=0, k1=0 524 result = SipHash._siphash24( 525 0x0706050403020100, 526 0x0f0e0d0c0b0a0908, 527 bytes(range(15)), 528 0, 15, 529 ) 530 # Known test vector for SipHash-2-4 with these keys and this input 531 assert result == 0xa129ca6149be45e5