|
| 1 | +import os |
| 2 | +import re |
| 3 | +import shutil |
| 4 | +import subprocess |
| 5 | +from unittest.mock import patch |
| 6 | + |
1 | 7 | import pytest
|
| 8 | +from quark import config |
| 9 | +from quark.utils.tools import (contains, descriptor_to_androguard_format, |
| 10 | + download_rizin, find_rizin_instance, |
| 11 | + get_rizin_version, remove_dup_list, |
| 12 | + update_rizin) |
| 13 | + |
| 14 | + |
| 15 | +@pytest.fixture(scope="module") |
| 16 | +def rizin_in_system_path(): |
| 17 | + path = shutil.which("rizin") |
| 18 | + assert path |
| 19 | + |
| 20 | + return path |
| 21 | + |
| 22 | + |
| 23 | +@pytest.fixture(scope="module") |
| 24 | +def rizin_version(rizin_in_system_path): |
| 25 | + try: |
| 26 | + process = subprocess.run( |
| 27 | + [rizin_in_system_path, "-v"], |
| 28 | + timeout=5, |
| 29 | + check=True, |
| 30 | + stdout=subprocess.PIPE, |
| 31 | + ) |
| 32 | + result = str(process.stdout) |
2 | 33 |
|
3 |
| -from quark.utils.tools import ( |
4 |
| - contains, |
5 |
| - descriptor_to_androguard_format, |
6 |
| - remove_dup_list, |
| 34 | + matched_versions = re.finditer( |
| 35 | + r"[0-9]+\.[0-9]+\.[0-9]+", result[: result.index("@")] |
| 36 | + ) |
| 37 | + first_matched = next(matched_versions, None) |
| 38 | + |
| 39 | + assert first_matched |
| 40 | + |
| 41 | + return first_matched.group(0) |
| 42 | + except TimeoutError: |
| 43 | + assert False |
| 44 | + except subprocess.CalledProcessError: |
| 45 | + assert False |
| 46 | + |
| 47 | + |
| 48 | +@pytest.fixture( |
| 49 | + scope="function", |
| 50 | + params=((True), (False)), |
7 | 51 | )
|
| 52 | +def disable_rizin_installation(request): |
| 53 | + return request.param |
8 | 54 |
|
9 | 55 |
|
10 | 56 | def test_remove_dup_list_with_invalid_arg():
|
@@ -112,3 +158,288 @@ def test_descriptor_to_androguard_format_with_combination():
|
112 | 158 | result = descriptor_to_androguard_format(descriptor)
|
113 | 159 |
|
114 | 160 | assert result == "(I Ljava/lang/String; [B J)"
|
| 161 | + |
| 162 | + |
| 163 | +def test_get_rizin_version_with_valid_path( |
| 164 | + rizin_in_system_path, rizin_version |
| 165 | +): |
| 166 | + expected_version = rizin_version |
| 167 | + |
| 168 | + found_version = get_rizin_version(rizin_in_system_path) |
| 169 | + |
| 170 | + assert found_version == expected_version |
| 171 | + |
| 172 | + |
| 173 | +def test_get_rizin_version_with_invalid_path(tmp_path): |
| 174 | + assert not get_rizin_version(tmp_path) |
| 175 | + |
| 176 | + |
| 177 | +def test_download_rizin_successfully(tmp_path): |
| 178 | + target_path = tmp_path / "rizin" |
| 179 | + |
| 180 | + download_rizin(target_path) |
| 181 | + |
| 182 | + assert os.access(target_path, os.F_OK | os.X_OK) |
| 183 | + |
| 184 | + |
| 185 | +def test_fail_to_download_rizin_due_to_unavailable_network(tmp_path): |
| 186 | + target_path = tmp_path / "rizin" |
| 187 | + |
| 188 | + with patch("subprocess.Popen") as mock: |
| 189 | + mock.side_effect = subprocess.CalledProcessError( |
| 190 | + "1", |
| 191 | + "mock command", |
| 192 | + stderr=b"fatal: unable to access 'https://github.com/rizinorg/rizin/'.", |
| 193 | + ) |
| 194 | + |
| 195 | + assert not download_rizin(target_path) |
| 196 | + |
| 197 | + |
| 198 | +def test_fail_to_download_rizin_due_to_unknown_errors(tmp_path): |
| 199 | + target_path = tmp_path / "rizin" |
| 200 | + |
| 201 | + with patch("subprocess.Popen") as mock: |
| 202 | + mock.side_effect = subprocess.CalledProcessError( |
| 203 | + "1", "mock command", stderr=b"" |
| 204 | + ) |
| 205 | + |
| 206 | + assert not download_rizin(target_path) |
| 207 | + |
| 208 | + |
| 209 | +def test_update_rizin(tmp_path): |
| 210 | + target_path = tmp_path / "rizin" |
| 211 | + target_commit = config.RIZIN_COMMIT |
| 212 | + |
| 213 | + download_rizin(target_path) |
| 214 | + |
| 215 | + update_rizin(target_path, target_commit) |
| 216 | + check_commit = subprocess.run( |
| 217 | + ["git", "rev-parse", "HEAD"], |
| 218 | + stdout=subprocess.PIPE, |
| 219 | + stderr=subprocess.PIPE, |
| 220 | + check=True, |
| 221 | + cwd=target_path, |
| 222 | + ) |
| 223 | + real_commit = check_commit.stdout.strip().decode() |
| 224 | + |
| 225 | + assert real_commit == target_commit |
| 226 | + assert os.access( |
| 227 | + target_path / "build" / "binrz" / "rizin" / "rizin", os.F_OK | os.X_OK |
| 228 | + ) |
| 229 | + |
| 230 | + |
| 231 | +def test_fail_to_update_rizin_due_to_any_errors(tmp_path): |
| 232 | + target_path = tmp_path / "rizin" |
| 233 | + target_commit = config.RIZIN_COMMIT |
| 234 | + |
| 235 | + with patch("subprocess.Popen") as mock: |
| 236 | + mock.side_effect = subprocess.CalledProcessError( |
| 237 | + "1", "mock command", stderr=b"Error message" |
| 238 | + ) |
| 239 | + |
| 240 | + assert not update_rizin(target_path, target_commit) |
| 241 | + |
| 242 | + |
| 243 | +def test_find_rizin_instance_in_system_path(rizin_in_system_path): |
| 244 | + rizin_path = find_rizin_instance() |
| 245 | + |
| 246 | + assert rizin_path == rizin_in_system_path |
| 247 | + |
| 248 | + |
| 249 | +def test_find_rizin_instance_installed_in_quark_directory(): |
| 250 | + rizin_source_path = "rizin_source_path" |
| 251 | + rizin_executable_path = rizin_source_path + "build/binrz/rizin/rizin" |
| 252 | + target_commit = "Unused" |
| 253 | + |
| 254 | + with patch("shutil.which") as mocked_which: |
| 255 | + # Pretent there is no Rizin instance installed in the system. |
| 256 | + mocked_which.return_value = None |
| 257 | + |
| 258 | + with patch( |
| 259 | + "quark.utils.tools.get_rizin_version" |
| 260 | + ) as mocked_get_version: |
| 261 | + # Pretent the Rizin instance installed in the Quark directory is compatible. |
| 262 | + mocked_get_version.return_value = config.COMPATIBLE_RAZIN_VERSIONS[ |
| 263 | + 0 |
| 264 | + ] |
| 265 | + |
| 266 | + # Must use the instance in the Quark directory. |
| 267 | + assert ( |
| 268 | + find_rizin_instance(rizin_source_path, target_commit) |
| 269 | + == rizin_executable_path |
| 270 | + ) |
| 271 | + |
| 272 | + mocked_which.assert_called() # Must check the system path first. |
| 273 | + mocked_get_version.assert_called() # Must check the version of the instance in the Quark directory. |
| 274 | + |
| 275 | + |
| 276 | +def test_find_outdated_rizin_instance_installed_in_quark_directory( |
| 277 | + disable_rizin_installation, |
| 278 | +): |
| 279 | + rizin_source_path = "rizin_source_path" |
| 280 | + rizin_executable_path = rizin_source_path + "build/binrz/rizin/rizin" |
| 281 | + target_commit = "Unused" |
| 282 | + |
| 283 | + with patch("shutil.which") as mocked_which: |
| 284 | + # Pretent there is no Rizin instance installed in the system. |
| 285 | + mocked_which.return_value = None |
| 286 | + |
| 287 | + with patch( |
| 288 | + "quark.utils.tools.get_rizin_version" |
| 289 | + ) as mocked_get_version: |
| 290 | + # Pretent the Rizin instance installed in the Quark directory is not compatible. |
| 291 | + mocked_get_version.return_value = "0.0.0" |
| 292 | + |
| 293 | + with patch( |
| 294 | + "quark.utils.tools.update_rizin" |
| 295 | + ) as mocked_update_rizin: |
| 296 | + # Pretent the upgrade is finished successfully. |
| 297 | + mocked_update_rizin.return_value = True |
| 298 | + |
| 299 | + # Must use the instance in the Quark directory. |
| 300 | + assert ( |
| 301 | + find_rizin_instance( |
| 302 | + rizin_source_path, |
| 303 | + target_commit, |
| 304 | + disable_rizin_installation, |
| 305 | + ) |
| 306 | + == rizin_executable_path |
| 307 | + ) |
| 308 | + |
| 309 | + mocked_which.assert_called() # Must check the system path first. |
| 310 | + mocked_get_version.assert_called() # Must check the version of the instance in the Quark directory. |
| 311 | + if disable_rizin_installation: |
| 312 | + mocked_update_rizin.assert_not_called() # Must not update the instance |
| 313 | + else: |
| 314 | + mocked_update_rizin.assert_called() # Must update the instance to a compatible version |
| 315 | + |
| 316 | + |
| 317 | +_compatible_trigger = None |
| 318 | + |
| 319 | + |
| 320 | +def _side_effort_for_downloading_rizin(arg): |
| 321 | + global _compatible_trigger |
| 322 | + _compatible_trigger = True |
| 323 | + return True |
| 324 | + |
| 325 | + |
| 326 | +def test_find_broken_rizin_instance_installed_in_quark_directory( |
| 327 | + disable_rizin_installation, |
| 328 | +): |
| 329 | + rizin_source_path = "rizin_source_path" |
| 330 | + rizin_executable_path = rizin_source_path + "build/binrz/rizin/rizin" |
| 331 | + target_commit = "Unused" |
| 332 | + |
| 333 | + with patch("shutil.which") as mocked_which: |
| 334 | + # Pretent there is no Rizin instance installed in the system. |
| 335 | + mocked_which.return_value = "rizin_installed_in_system" |
| 336 | + |
| 337 | + with patch( |
| 338 | + "quark.utils.tools.get_rizin_version" |
| 339 | + ) as mocked_get_version: |
| 340 | + # Pretent - |
| 341 | + # 1. the Rizin instance in the system path is not compatible |
| 342 | + # 2. the Rizin instance in the Quark directory is broken. |
| 343 | + mocked_get_version.side_effect = ( |
| 344 | + lambda x: "0.0.0" |
| 345 | + if x == "rizin_installed_in_system" |
| 346 | + else _compatible_trigger |
| 347 | + ) |
| 348 | + |
| 349 | + with patch( |
| 350 | + "quark.utils.tools.download_rizin" |
| 351 | + ) as mocked_download_rizin: |
| 352 | + # Pretent we can download the source code successfully. |
| 353 | + mocked_download_rizin.side_effect = ( |
| 354 | + _side_effort_for_downloading_rizin |
| 355 | + ) |
| 356 | + |
| 357 | + with patch( |
| 358 | + "quark.utils.tools.update_rizin" |
| 359 | + ) as mocked_update_rizin: |
| 360 | + # Pretent we can finish the upgrade successfully. |
| 361 | + mocked_update_rizin.return_value = True |
| 362 | + |
| 363 | + result = find_rizin_instance( |
| 364 | + rizin_source_path, |
| 365 | + target_commit, |
| 366 | + disable_rizin_installation, |
| 367 | + ) |
| 368 | + if disable_rizin_installation: |
| 369 | + # No Rizin instance exists |
| 370 | + assert result == None |
| 371 | + else: |
| 372 | + # Must use the instance in the Quark directory. |
| 373 | + assert result == rizin_executable_path |
| 374 | + |
| 375 | + mocked_which.assert_called() # Must check the system path first. |
| 376 | + mocked_get_version.assert_called() # Must check the version of the instance in the Quark directory. |
| 377 | + |
| 378 | + if disable_rizin_installation: |
| 379 | + mocked_download_rizin.assert_not_called() # Must not download the source code. |
| 380 | + mocked_update_rizin.assert_not_called() # Must not update and compile a Rizin instance. |
| 381 | + else: |
| 382 | + mocked_download_rizin.assert_called() # Must download the source code. |
| 383 | + mocked_update_rizin.assert_called() # Must update and compile a Rizin instance. |
| 384 | + |
| 385 | + |
| 386 | +def test_find_rizin_instance_failed_to_download_the_source(): |
| 387 | + rizin_source_path = "rizin_source_path" |
| 388 | + target_commit = "Unused" |
| 389 | + |
| 390 | + with patch("shutil.which") as mocked_which: |
| 391 | + # Pretent there is no Rizin instance installed in the system. |
| 392 | + mocked_which.return_value = None |
| 393 | + |
| 394 | + with patch( |
| 395 | + "quark.utils.tools.get_rizin_version" |
| 396 | + ) as mocked_get_version: |
| 397 | + # Pretent the Rizin instance installed in the Quark directory is broken. |
| 398 | + mocked_get_version.return_value = None |
| 399 | + |
| 400 | + with patch( |
| 401 | + "quark.utils.tools.download_rizin" |
| 402 | + ) as mocked_download_rizin: |
| 403 | + # Fail to download the source of Rizin. |
| 404 | + mocked_download_rizin.return_value = False |
| 405 | + |
| 406 | + # Must use the instance in the Quark directory. |
| 407 | + assert ( |
| 408 | + find_rizin_instance(rizin_source_path, target_commit) |
| 409 | + == None |
| 410 | + ) |
| 411 | + |
| 412 | + mocked_which.assert_called() # Must check the system path first. |
| 413 | + mocked_get_version.assert_called() # Must check the version of the instance in the Quark directory. |
| 414 | + mocked_download_rizin.assert_called() # Must try to download the source code of the Rizin. |
| 415 | + |
| 416 | + |
| 417 | +def test_find_rizin_instance_failed_to_compile_or_update_the_source(): |
| 418 | + rizin_source_path = "rizin_source_path" |
| 419 | + target_commit = "Unused" |
| 420 | + |
| 421 | + with patch("shutil.which") as mocked_which: |
| 422 | + # Pretent there is no Rizin instance installed in the system. |
| 423 | + mocked_which.return_value = None |
| 424 | + |
| 425 | + with patch( |
| 426 | + "quark.utils.tools.get_rizin_version" |
| 427 | + ) as mocked_get_version: |
| 428 | + # Pretent the Rizin instance installed in the Quark directory is not compatible. |
| 429 | + mocked_get_version.return_value = "0.0.0" |
| 430 | + |
| 431 | + with patch( |
| 432 | + "quark.utils.tools.update_rizin" |
| 433 | + ) as mocked_update_rizin: |
| 434 | + # Pretent the upgrade is finished successfully. |
| 435 | + mocked_update_rizin.return_value = False |
| 436 | + |
| 437 | + # Must use the instance in the Quark directory. |
| 438 | + assert ( |
| 439 | + find_rizin_instance(rizin_source_path, target_commit) |
| 440 | + == None |
| 441 | + ) |
| 442 | + |
| 443 | + mocked_which.assert_called() # Must check the system path first. |
| 444 | + mocked_get_version.assert_called() # Must check the version of the instance in the Quark directory. |
| 445 | + mocked_update_rizin.assert_called() # Must try to update and compile a Rizin instance. |
0 commit comments