test_runner.py 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147
  1. import importlib.util
  2. from pathlib import Path
  3. import subprocess
  4. import sys
  5. import tempfile
  6. import unittest
  7. from unittest import mock
  8. RUNNER_PATH = Path(__file__).parents[1] / "runner.py"
  9. SPEC = importlib.util.spec_from_file_location("legado_android_runner", RUNNER_PATH)
  10. runner = importlib.util.module_from_spec(SPEC)
  11. assert SPEC.loader is not None
  12. SPEC.loader.exec_module(runner)
  13. class ScenarioValidationTest(unittest.TestCase):
  14. def test_repository_scenario_is_valid(self):
  15. scenario = runner.load_scenario("reader/page-turn")
  16. self.assertEqual("long-chapter-v1", scenario["fixture"])
  17. def test_rejects_unknown_action(self):
  18. scenario = {
  19. "schemaVersion": 1,
  20. "name": "reader/page-turn",
  21. "fixture": "long-chapter-v1",
  22. "entry": {"type": "reader"},
  23. "readyMarker": "marker",
  24. "actions": [{"type": "tap"}],
  25. "assertions": ["reader_ready"],
  26. }
  27. with self.assertRaises(runner.ScenarioError):
  28. runner.validate_scenario(scenario)
  29. def test_rejects_missing_fixture(self):
  30. with self.assertRaises(runner.ScenarioError):
  31. runner.validate_fixture("missing-v1")
  32. def test_rejects_invalid_fixture_before_device_work(self):
  33. fixture = {
  34. "schemaVersion": 1,
  35. "id": "broken-v1",
  36. "resetPolicy": "replace",
  37. "book": {
  38. "bookUrl": "debug://book",
  39. "name": "broken",
  40. "author": "debug",
  41. "pageMode": "simulation",
  42. "startChapter": 0,
  43. "startPosition": 0,
  44. },
  45. "chapters": [{"title": "chapter", "readyMarker": "ready", "paragraph": "text", "repeat": 0}],
  46. }
  47. with self.assertRaises(runner.ScenarioError):
  48. runner.validate_fixture_data(fixture, "broken-v1")
  49. class FailureParsingTest(unittest.TestCase):
  50. def test_ignores_crash_from_another_package(self):
  51. logcat = """FATAL EXCEPTION: main
  52. Process: com.example.other, PID: 42
  53. java.lang.IllegalStateException: unrelated
  54. """
  55. self.assertEqual("", runner.extract_crash(logcat))
  56. def test_parses_app_frame_as_inference(self):
  57. crash = """FATAL EXCEPTION: main
  58. java.lang.IndexOutOfBoundsException: Index 18 out of bounds
  59. at io.legado.app.ui.book.read.ReadBookViewModel.loadChapter(ReadBookViewModel.kt:418)
  60. """
  61. failure = runner.parse_failure(crash)
  62. self.assertEqual("uncaught_exception", failure["category"])
  63. self.assertEqual("IndexOutOfBoundsException", failure["exceptionType"])
  64. self.assertEqual("medium", failure["suspectedFrame"]["confidence"])
  65. self.assertEqual(418, failure["suspectedFrame"]["line"])
  66. def test_prefers_deepest_caused_by_as_root_cause_candidate(self):
  67. crash = """FATAL EXCEPTION: main
  68. java.lang.RuntimeException: wrapper
  69. Caused by: java.lang.IllegalStateException: middle
  70. Caused by: java.lang.IndexOutOfBoundsException: root
  71. """
  72. failure = runner.parse_failure(crash)
  73. self.assertEqual("IndexOutOfBoundsException", failure["exceptionType"])
  74. self.assertEqual("root", failure["message"])
  75. self.assertEqual(["fatal_exception"], failure["detectedSignals"])
  76. def test_crash_has_priority_over_assertion_failure(self):
  77. scenario = runner.load_scenario("reader/page-turn")
  78. summary, exit_code = runner.make_summary(
  79. session_id="test",
  80. scenario=scenario,
  81. fixture="long-chapter-v1",
  82. build={"status": "passed"},
  83. ready=False,
  84. activity_visible=False,
  85. content_changed=False,
  86. action_count=0,
  87. failure={"category": "uncaught_exception", "stacktraceArtifact": "crash.txt"},
  88. )
  89. self.assertEqual(runner.EXIT_CRASH, exit_code)
  90. self.assertEqual(runner.EXIT_CRASH, summary["exitCode"])
  91. class CommandDecodingTest(unittest.TestCase):
  92. def test_replaces_invalid_utf8_in_command_output(self):
  93. result = runner.run_command(
  94. [sys.executable, "-c", "import sys; sys.stdout.buffer.write(b'log\\xc0tail')"],
  95. )
  96. self.assertEqual("log\ufffdtail", result.stdout)
  97. @mock.patch.object(runner, "cleanup_device")
  98. @mock.patch.object(runner, "run_command")
  99. def test_install_retries_with_streaming_after_timeout(self, run_command, cleanup_device):
  100. run_command.side_effect = [
  101. subprocess.TimeoutExpired(["adb", "install"], 120),
  102. subprocess.CompletedProcess(["adb", "install"], 0, "Success\n"),
  103. ]
  104. with tempfile.TemporaryDirectory() as directory:
  105. mode = runner.install_apk("device", Path("app.apk"), Path(directory))
  106. install_log = (Path(directory) / "install.log").read_text(encoding="utf-8")
  107. self.assertEqual("streaming_retry", mode)
  108. self.assertIn("timeout=120s", install_log)
  109. cleanup_device.assert_called_once_with("device")
  110. class ReaderContentTest(unittest.TestCase):
  111. def test_extracts_read_view_content_only(self):
  112. dump = """<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>
  113. <hierarchy rotation="0">
  114. <node class="android.widget.TextView" package="io.legato.kazusa.debug" content-desc="22:30" />
  115. <node class="io.legado.app.ui.book.read.page.ReadView" package="io.legato.kazusa.debug" content-desc="LEGADO_DEBUG_LONG_CHAPTER_V1 page body" />
  116. </hierarchy>
  117. UI hierarchy dumped to: /dev/tty
  118. """
  119. self.assertEqual(
  120. "LEGADO_DEBUG_LONG_CHAPTER_V1 page body",
  121. runner.extract_reader_content(dump),
  122. )
  123. def test_returns_empty_when_read_view_is_absent(self):
  124. dump = """<?xml version="1.0"?><hierarchy><node class="android.view.View" /></hierarchy>"""
  125. self.assertEqual("", runner.extract_reader_content(dump))
  126. if __name__ == "__main__":
  127. unittest.main()