test_firmware_server.py 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205
  1. import json
  2. import lzma
  3. from pathlib import Path
  4. from unittest.mock import Mock, patch
  5. from metadata_manager import ManifestClient, ManifestIndex, ManifestJSON
  6. from metadata_manager.firmware_server.client import _CacheMeta
  7. from metadata_manager.firmware_server.index import latest_features_txt_url
  8. FIXTURES_DIR = Path(__file__).parent / "fixtures"
  9. SAMPLE_MANIFEST = json.loads((FIXTURES_DIR / "manifest_sample.json").read_text())
  10. SAMPLE_JSON_BYTES = json.dumps(SAMPLE_MANIFEST).encode("utf-8")
  11. MANIFEST_XZ_URL = "https://firmware.ardupilot.org/manifest.json.xz"
  12. class TestManifestIndex:
  13. def test_builds_releases_for_copter_heli_and_tracker(self):
  14. index = ManifestIndex.build(SAMPLE_MANIFEST)
  15. copter = index.get_releases("copter")
  16. stable = [r for r in copter if r.release_type == "stable" and r.version_number == "4.6.3"]
  17. assert len(stable) == 1
  18. assert stable[0].commit_reference.startswith("aaaa")
  19. heli = index.get_releases("heli")
  20. heli_stable = [r for r in heli if r.release_type == "stable" and r.version_number == "4.6.3"]
  21. assert len(heli_stable) == 1
  22. assert heli_stable[0].commit_reference.startswith("bbbb")
  23. tracker = index.get_releases("tracker")
  24. assert any(r.version_number == "4.6.3" for r in tracker)
  25. latest = [r for r in copter if r.release_type == "latest"]
  26. assert len(latest) == 1
  27. assert latest[0].commit_reference.startswith("eeee")
  28. assert latest[0].version_number == "NA"
  29. def test_indexes_board_artifacts_for_vehicle_version_board(self):
  30. index = ManifestIndex.build(SAMPLE_MANIFEST)
  31. copter_artifacts = index.get_board_artifacts(
  32. "copter", "stable", "4.6.3", "CubeOrange"
  33. )
  34. assert len(copter_artifacts) == 1
  35. assert copter_artifacts[0].name == "arducopter.apj"
  36. assert copter_artifacts[0].format == "apj"
  37. assert copter_artifacts[0].url.endswith("/Copter/stable-4.6.3/CubeOrange/arducopter.apj")
  38. heli_artifacts = index.get_board_artifacts(
  39. "heli", "stable", "4.6.3", "CubeOrange"
  40. )
  41. assert len(heli_artifacts) == 1
  42. assert heli_artifacts[0].name == "arducopter-heli.apj"
  43. latest_artifacts = index.get_board_artifacts(
  44. "copter", "latest", "NA", "CubeOrange"
  45. )
  46. assert len(latest_artifacts) == 1
  47. assert latest_artifacts[0].url.endswith("/Copter/latest/CubeOrange/arducopter.apj")
  48. def test_board_artifacts_dedupe_generic_stable_alias(self):
  49. index = ManifestIndex.build(SAMPLE_MANIFEST)
  50. copter_artifacts = index.get_board_artifacts(
  51. "copter", "stable", "4.6.3", "CubeOrange"
  52. )
  53. assert len(copter_artifacts) == 1
  54. assert copter_artifacts[0].url.endswith("/Copter/stable-4.6.3/CubeOrange/arducopter.apj")
  55. heli_artifacts = index.get_board_artifacts(
  56. "heli", "stable", "4.6.3", "CubeOrange"
  57. )
  58. assert len(heli_artifacts) == 1
  59. assert heli_artifacts[0].name == "arducopter-heli.apj"
  60. latest_artifacts = index.get_board_artifacts(
  61. "copter", "latest", "NA", "CubeOrange"
  62. )
  63. assert len(latest_artifacts) == 1
  64. assert latest_artifacts[0].url.endswith("/Copter/latest/CubeOrange/arducopter.apj")
  65. def test_board_artifacts_missing_returns_empty_list(self):
  66. index = ManifestIndex.build(SAMPLE_MANIFEST)
  67. assert index.get_board_artifacts("copter", "stable", "4.6.3", "UnknownBoard") == []
  68. def test_get_features_txt_url_from_manifest_artifact(self):
  69. index = ManifestIndex.build(SAMPLE_MANIFEST)
  70. url = index.get_features_txt_url("copter", "stable", "4.6.3", "CubeOrange")
  71. assert url == (
  72. "https://firmware.ardupilot.org/Copter/stable-4.6.3/"
  73. "CubeOrange/features.txt"
  74. )
  75. heli_url = index.get_features_txt_url("heli", "stable", "4.6.3", "CubeOrange")
  76. assert heli_url == (
  77. "https://firmware.ardupilot.org/Copter/stable-4.6.3/"
  78. "CubeOrange-heli/features.txt"
  79. )
  80. def test_get_features_txt_url_missing_board_returns_none(self):
  81. index = ManifestIndex.build(SAMPLE_MANIFEST)
  82. assert index.get_features_txt_url(
  83. "copter", "stable", "4.6.3", "UnknownBoard"
  84. ) is None
  85. class TestLatestFeaturesTxtUrl:
  86. def test_copter_board(self):
  87. assert latest_features_txt_url("copter", "CubeOrange") == (
  88. "https://firmware.ardupilot.org/Copter/latest/CubeOrange/features.txt"
  89. )
  90. def test_heli_board(self):
  91. assert latest_features_txt_url("heli", "CubeOrange") == (
  92. "https://firmware.ardupilot.org/Copter/latest/CubeOrange-heli/features.txt"
  93. )
  94. class TestManifestJSONFeaturesUrl:
  95. def test_tag_release_uses_latest_url(self):
  96. manifest_json = ManifestJSON(url="https://example.com/manifest.json", cache_dir="/tmp")
  97. assert manifest_json.get_features_txt_url(
  98. "copter", "tag", "my-feature", "CubeOrange"
  99. ) == latest_features_txt_url("copter", "CubeOrange")
  100. def test_stable_release_uses_manifest_index(self):
  101. manifest_json = ManifestJSON(url="https://example.com/manifest.json", cache_dir="/tmp")
  102. manifest_json._index = ManifestIndex.build(SAMPLE_MANIFEST)
  103. assert manifest_json.get_features_txt_url(
  104. "copter", "stable", "4.6.3", "CubeOrange"
  105. ) == (
  106. "https://firmware.ardupilot.org/Copter/stable-4.6.3/"
  107. "CubeOrange/features.txt"
  108. )
  109. def test_unavailable_manifest_returns_none_for_stable(self):
  110. manifest_json = ManifestJSON(url="https://example.com/manifest.json", cache_dir="/tmp")
  111. assert manifest_json.get_features_txt_url(
  112. "copter", "stable", "4.6.3", "CubeOrange"
  113. ) is None
  114. class TestManifestClientCache:
  115. def test_uses_cache_on_304(self, tmp_path):
  116. client = ManifestClient(
  117. url=MANIFEST_XZ_URL,
  118. cache_dir=str(tmp_path),
  119. )
  120. client._write_cache(
  121. b'{"format-version":"1.0.0","firmware":[]}',
  122. _CacheMeta(etag='"abc"', last_modified="Mon, 01 Jan 2024 00:00:00 GMT"),
  123. )
  124. response = Mock(status_code=304, headers={}, content=b"")
  125. with patch(
  126. "metadata_manager.firmware_server.client.requests.get",
  127. return_value=response,
  128. ) as mock_get:
  129. raw = client.fetch_raw()
  130. assert raw == b'{"format-version":"1.0.0","firmware":[]}'
  131. mock_get.assert_called_once_with(
  132. MANIFEST_XZ_URL,
  133. headers={
  134. "User-Agent": "CustomBuild/1.0",
  135. "If-None-Match": '"abc"',
  136. "If-Modified-Since": "Mon, 01 Jan 2024 00:00:00 GMT",
  137. },
  138. timeout=120,
  139. )
  140. def test_download_decompresses_xz_manifest(self, tmp_path):
  141. client = ManifestClient(
  142. url=MANIFEST_XZ_URL,
  143. cache_dir=str(tmp_path),
  144. )
  145. compressed = lzma.compress(SAMPLE_JSON_BYTES)
  146. response = Mock(
  147. status_code=200,
  148. content=compressed,
  149. headers={
  150. "ETag": '"etag123"',
  151. "Last-Modified": "Mon, 01 Jan 2024 00:00:00 GMT",
  152. },
  153. )
  154. response.raise_for_status = Mock()
  155. with patch(
  156. "metadata_manager.firmware_server.client.requests.get",
  157. return_value=response,
  158. ):
  159. result = client.fetch()
  160. assert result == SAMPLE_MANIFEST
  161. assert client.cache_path.read_bytes() == SAMPLE_JSON_BYTES
  162. meta = _CacheMeta.from_dict(
  163. json.loads(client.meta_path.read_text(encoding="utf-8"))
  164. )
  165. assert meta.etag == '"etag123"'
  166. assert meta.last_modified == "Mon, 01 Jan 2024 00:00:00 GMT"