test_vehicles_service.py 55 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272
  1. """
  2. Tests for the Vehicles Service.
  3. """
  4. import pytest
  5. from unittest.mock import Mock
  6. from metadata_manager import (
  7. BoardArtifact,
  8. RemoteInfo,
  9. Vehicle,
  10. VersionInfo,
  11. )
  12. from web.services.vehicles import VehiclesService
  13. @pytest.fixture
  14. def mock_manifest_json():
  15. mock = Mock()
  16. mock.get_features_txt_url.return_value = None
  17. return mock
  18. @pytest.fixture
  19. def mock_features_txt_client():
  20. mock = Mock()
  21. mock.get_defaults.return_value = None
  22. return mock
  23. @pytest.fixture
  24. def service(
  25. mock_vehicles_manager,
  26. mock_versions_manager,
  27. mock_ap_src_metadata_fetcher,
  28. mock_manifest_json,
  29. mock_features_txt_client,
  30. mock_git_repo,
  31. ):
  32. return VehiclesService(
  33. vehicle_manager=mock_vehicles_manager,
  34. versions_manager=mock_versions_manager,
  35. ap_src_metadata_fetcher=mock_ap_src_metadata_fetcher,
  36. manifest_json=mock_manifest_json,
  37. features_txt_client=mock_features_txt_client,
  38. repo=mock_git_repo,
  39. )
  40. class TestVehiclesService:
  41. """Test suite for VehiclesService."""
  42. # Tests for get_all_vehicles
  43. def test_get_all_vehicles_returns_all(self, service, mock_vehicles_manager):
  44. """Test fetching all vehicles returns correct count and values."""
  45. mock_vehicles_manager.get_all_vehicles.return_value = [
  46. Vehicle(
  47. id="copter",
  48. name="Copter",
  49. ap_source_subdir="ArduCopter",
  50. waf_build_command="copter"
  51. ),
  52. Vehicle(
  53. id="plane",
  54. name="Plane",
  55. ap_source_subdir="ArduPlane",
  56. waf_build_command="plane"
  57. ),
  58. ]
  59. vehicles = service.get_all_vehicles()
  60. assert len(vehicles) == 2
  61. assert vehicles[0].id == "copter"
  62. assert vehicles[0].name == "Copter"
  63. assert vehicles[1].id == "plane"
  64. assert vehicles[1].name == "Plane"
  65. def test_get_all_vehicles_empty(self, service, mock_vehicles_manager):
  66. """Test fetching all vehicles when none exist."""
  67. mock_vehicles_manager.get_all_vehicles.return_value = []
  68. vehicles = service.get_all_vehicles()
  69. assert vehicles == []
  70. def test_get_all_vehicles_single(self, service, mock_vehicles_manager):
  71. """Test fetching all vehicles when only one exists."""
  72. mock_vehicles_manager.get_all_vehicles.return_value = [
  73. Vehicle(
  74. id="copter",
  75. name="Copter",
  76. ap_source_subdir="ArduCopter",
  77. waf_build_command="copter"
  78. ),
  79. ]
  80. vehicles = service.get_all_vehicles()
  81. assert len(vehicles) == 1
  82. assert vehicles[0].id == "copter"
  83. def test_get_all_vehicles_sorted_by_name(self, service, mock_vehicles_manager):
  84. """Test fetching all vehicles returns them sorted by name."""
  85. mock_vehicles_manager.get_all_vehicles.return_value = [
  86. Vehicle(
  87. id="plane",
  88. name="Plane",
  89. ap_source_subdir="ArduPlane",
  90. waf_build_command="plane"
  91. ),
  92. Vehicle(
  93. id="copter",
  94. name="Copter",
  95. ap_source_subdir="ArduCopter",
  96. waf_build_command="copter"
  97. ),
  98. Vehicle(
  99. id="rover",
  100. name="Rover",
  101. ap_source_subdir="ArduRover",
  102. waf_build_command="rover"
  103. ),
  104. ]
  105. vehicles = service.get_all_vehicles()
  106. names = [v.name for v in vehicles]
  107. assert names == sorted(names)
  108. def test_get_all_vehicles_calls_manager_once(self, service, mock_vehicles_manager):
  109. """Test that get_all_vehicles calls the manager exactly once."""
  110. mock_vehicles_manager.get_all_vehicles.return_value = []
  111. service.get_all_vehicles()
  112. mock_vehicles_manager.get_all_vehicles.assert_called_once_with()
  113. # Tests for get_vehicle
  114. def test_get_vehicle_found(self, service, mock_vehicles_manager):
  115. """Test fetching a specific vehicle that exists."""
  116. mock_vehicles_manager.get_vehicle_by_id.return_value = Vehicle(
  117. id="copter",
  118. name="Copter",
  119. ap_source_subdir="ArduCopter",
  120. waf_build_command="copter"
  121. )
  122. vehicle = service.get_vehicle("copter")
  123. assert vehicle is not None
  124. assert vehicle.id == "copter"
  125. assert vehicle.name == "Copter"
  126. def test_get_vehicle_not_found(self, service, mock_vehicles_manager):
  127. """Test fetching a specific vehicle that does not exist."""
  128. mock_vehicles_manager.get_vehicle_by_id.return_value = None
  129. vehicle = service.get_vehicle("copter")
  130. assert vehicle is None
  131. def test_get_vehicle_calls_manager_with_correct_id(self, service, mock_vehicles_manager):
  132. """Test that get_vehicle calls manager with the provided ID."""
  133. mock_vehicles_manager.get_vehicle_by_id.return_value = None
  134. service.get_vehicle("copter")
  135. mock_vehicles_manager.get_vehicle_by_id.assert_called_once_with("copter")
  136. # Tests for get_versions
  137. def test_get_versions_empty(self, service, mock_versions_manager):
  138. """Test that an empty list is returned when no versions exist."""
  139. mock_versions_manager.get_versions_for_vehicle.return_value = []
  140. versions = service.get_versions("copter")
  141. assert versions == []
  142. def test_get_versions_single(self, service, mock_versions_manager):
  143. """Test fetching versions when only one version exists."""
  144. mock_versions_manager.get_versions_for_vehicle.return_value = [
  145. VersionInfo(
  146. remote_info=RemoteInfo(name="ardupilot", url="https://github.com/ArduPilot/ardupilot.git"),
  147. commit_ref="refs/heads/master",
  148. release_type="latest",
  149. version_number="NA",
  150. ),
  151. ]
  152. versions = service.get_versions("copter")
  153. assert len(versions) == 1
  154. def test_get_versions_many(self, service, mock_versions_manager):
  155. """Test fetching versions when multiple versions exist."""
  156. mock_versions_manager.get_versions_for_vehicle.return_value = [
  157. VersionInfo(
  158. remote_info=RemoteInfo(name="ardupilot", url="https://github.com/ArduPilot/ardupilot.git"),
  159. commit_ref="refs/heads/master",
  160. release_type="latest",
  161. version_number="NA",
  162. ),
  163. VersionInfo(
  164. remote_info=RemoteInfo(name="ardupilot", url="https://github.com/ArduPilot/ardupilot.git"),
  165. commit_ref="refs/tags/Copter-4.5.0",
  166. release_type="stable",
  167. version_number="4.5.0",
  168. ),
  169. VersionInfo(
  170. remote_info=RemoteInfo(name="ardupilot", url="https://github.com/ArduPilot/ardupilot.git"),
  171. commit_ref="refs/tags/Copter-4.6.0-beta",
  172. release_type="beta",
  173. version_number="4.6.0",
  174. ),
  175. ]
  176. versions = service.get_versions("copter")
  177. assert len(versions) == 3
  178. def test_get_versions_sorted_by_name(self, service, mock_versions_manager):
  179. """Test that versions are returned sorted by their display name."""
  180. mock_versions_manager.get_versions_for_vehicle.return_value = [
  181. VersionInfo(
  182. remote_info=RemoteInfo(name="ardupilot", url="https://github.com/ArduPilot/ardupilot.git"),
  183. commit_ref="refs/tags/Copter-4.5.0",
  184. release_type="stable",
  185. version_number="4.5.0",
  186. ),
  187. VersionInfo(
  188. remote_info=RemoteInfo(name="ardupilot", url="https://github.com/ArduPilot/ardupilot.git"),
  189. commit_ref="refs/heads/master",
  190. release_type="latest",
  191. version_number="NA",
  192. ),
  193. VersionInfo(
  194. remote_info=RemoteInfo(name="ardupilot", url="https://github.com/ArduPilot/ardupilot.git"),
  195. commit_ref="refs/tags/Copter-4.6.0-beta",
  196. release_type="beta",
  197. version_number="4.6.0",
  198. ),
  199. ]
  200. versions = service.get_versions("copter")
  201. names = [v.name for v in versions]
  202. assert names == sorted(names)
  203. def test_get_versions_calls_fetcher_once_with_correct_vehicle_id(
  204. self, service, mock_versions_manager
  205. ):
  206. """Test that get_versions calls the fetcher exactly once with the correct vehicle_id."""
  207. mock_versions_manager.get_versions_for_vehicle.return_value = []
  208. service.get_versions("copter")
  209. mock_versions_manager.get_versions_for_vehicle.assert_called_once_with(
  210. vehicle_id="copter"
  211. )
  212. def test_get_versions_type_filter_keeps_matching(
  213. self, service, mock_versions_manager
  214. ):
  215. """Test that type_filter returns only versions of the specified type."""
  216. mock_versions_manager.get_versions_for_vehicle.return_value = [
  217. VersionInfo(
  218. remote_info=RemoteInfo(name="ardupilot", url="https://github.com/ArduPilot/ardupilot.git"),
  219. commit_ref="refs/tags/Copter-4.5.0",
  220. release_type="stable",
  221. version_number="4.5.0",
  222. ),
  223. VersionInfo(
  224. remote_info=RemoteInfo(name="ardupilot", url="https://github.com/ArduPilot/ardupilot.git"),
  225. commit_ref="refs/tags/Copter-4.6.0-beta",
  226. release_type="beta",
  227. version_number="4.6.0",
  228. ),
  229. VersionInfo(
  230. remote_info=RemoteInfo(name="ardupilot", url="https://github.com/ArduPilot/ardupilot.git"),
  231. commit_ref="refs/heads/master",
  232. release_type="latest",
  233. version_number="NA",
  234. ),
  235. ]
  236. versions = service.get_versions("copter", type_filter="stable")
  237. assert len(versions) == 1
  238. assert versions[0].type == "stable"
  239. def test_get_versions_type_filter_excludes_non_matching(
  240. self, service, mock_versions_manager
  241. ):
  242. """Test that type_filter excludes versions that do not match."""
  243. mock_versions_manager.get_versions_for_vehicle.return_value = [
  244. VersionInfo(
  245. remote_info=RemoteInfo(name="ardupilot", url="https://github.com/ArduPilot/ardupilot.git"),
  246. commit_ref="refs/tags/Copter-4.5.0",
  247. release_type="stable",
  248. version_number="4.5.0",
  249. ),
  250. VersionInfo(
  251. remote_info=RemoteInfo(name="ardupilot", url="https://github.com/ArduPilot/ardupilot.git"),
  252. commit_ref="refs/tags/Copter-4.6.0-beta",
  253. release_type="beta",
  254. version_number="4.6.0",
  255. ),
  256. ]
  257. versions = service.get_versions("copter", type_filter="latest")
  258. assert versions == []
  259. def test_get_versions_type_filter_none_returns_all(
  260. self, service, mock_versions_manager
  261. ):
  262. """Test that passing no type_filter returns all versions."""
  263. mock_versions_manager.get_versions_for_vehicle.return_value = [
  264. VersionInfo(
  265. remote_info=RemoteInfo(name="ardupilot", url="https://github.com/ArduPilot/ardupilot.git"),
  266. commit_ref="refs/tags/Copter-4.5.0",
  267. release_type="stable",
  268. version_number="4.5.0",
  269. ),
  270. VersionInfo(
  271. remote_info=RemoteInfo(name="ardupilot", url="https://github.com/ArduPilot/ardupilot.git"),
  272. commit_ref="refs/tags/Copter-4.6.0-beta",
  273. release_type="beta",
  274. version_number="4.6.0",
  275. ),
  276. VersionInfo(
  277. remote_info=RemoteInfo(name="ardupilot", url="https://github.com/ArduPilot/ardupilot.git"),
  278. commit_ref="refs/heads/master",
  279. release_type="latest",
  280. version_number="NA",
  281. ),
  282. ]
  283. versions = service.get_versions("copter")
  284. assert len(versions) == 3
  285. def test_get_versions_type_filter_multiple_matches(
  286. self, service, mock_versions_manager
  287. ):
  288. """Test that type_filter returns all versions matching the type when there are multiple."""
  289. mock_versions_manager.get_versions_for_vehicle.return_value = [
  290. VersionInfo(
  291. remote_info=RemoteInfo(name="ardupilot", url="https://github.com/ArduPilot/ardupilot.git"),
  292. commit_ref="refs/tags/Copter-4.4.0",
  293. release_type="stable",
  294. version_number="4.4.0",
  295. ),
  296. VersionInfo(
  297. remote_info=RemoteInfo(name="ardupilot", url="https://github.com/ArduPilot/ardupilot.git"),
  298. commit_ref="refs/tags/Copter-4.5.0",
  299. release_type="stable",
  300. version_number="4.5.0",
  301. ),
  302. VersionInfo(
  303. remote_info=RemoteInfo(name="ardupilot", url="https://github.com/ArduPilot/ardupilot.git"),
  304. commit_ref="refs/heads/master",
  305. release_type="latest",
  306. version_number="NA",
  307. ),
  308. ]
  309. versions = service.get_versions("copter", type_filter="stable")
  310. assert len(versions) == 2
  311. assert all(v.type == "stable" for v in versions)
  312. def test_get_versions_latest_name_format(
  313. self, service, mock_versions_manager
  314. ):
  315. """Test that latest versions have the correct display name format."""
  316. mock_versions_manager.get_versions_for_vehicle.return_value = [
  317. VersionInfo(
  318. remote_info=RemoteInfo(name="ardupilot", url="https://github.com/ArduPilot/ardupilot.git"),
  319. commit_ref="refs/heads/master",
  320. release_type="latest",
  321. version_number="NA",
  322. ),
  323. ]
  324. versions = service.get_versions("copter")
  325. assert versions[0].name == "Latest (ardupilot)"
  326. def test_get_versions_non_latest_name_format(
  327. self, service, mock_versions_manager
  328. ):
  329. """Test that non-latest versions have the correct display name format."""
  330. mock_versions_manager.get_versions_for_vehicle.return_value = [
  331. VersionInfo(
  332. remote_info=RemoteInfo(name="ardupilot", url="https://github.com/ArduPilot/ardupilot.git"),
  333. commit_ref="refs/tags/Copter-4.5.0",
  334. release_type="stable",
  335. version_number="4.5.0",
  336. ),
  337. ]
  338. versions = service.get_versions("copter")
  339. assert versions[0].name == "stable 4.5.0 (ardupilot)"
  340. # Tests for get_version
  341. def test_get_version_found(self, service, mock_versions_manager):
  342. """Test that the correct version is returned when it exists."""
  343. version_info = VersionInfo(
  344. remote_info=RemoteInfo(name="ardupilot", url="https://github.com/ArduPilot/ardupilot.git"),
  345. commit_ref="refs/tags/Copter-4.5.0",
  346. release_type="stable",
  347. version_number="4.5.0",
  348. )
  349. mock_versions_manager.get_versions_for_vehicle.return_value = [version_info]
  350. result = service.get_version("copter", version_info.version_id)
  351. assert result is not None
  352. assert result.id == version_info.version_id
  353. def test_get_version_not_found(self, service, mock_versions_manager):
  354. """Test that None is returned when the version does not exist."""
  355. mock_versions_manager.get_versions_for_vehicle.return_value = [
  356. VersionInfo(
  357. remote_info=RemoteInfo(name="ardupilot", url="https://github.com/ArduPilot/ardupilot.git"),
  358. commit_ref="refs/tags/Copter-4.5.0",
  359. release_type="stable",
  360. version_number="4.5.0",
  361. ),
  362. ]
  363. result = service.get_version("copter", "nonexistent-version-id")
  364. assert result is None
  365. def test_get_version_no_versions_available(self, service, mock_versions_manager):
  366. """Test that None is returned when there are no versions at all."""
  367. mock_versions_manager.get_versions_for_vehicle.return_value = []
  368. result = service.get_version("copter", "any-version-id")
  369. assert result is None
  370. def test_get_version_returns_correct_match_among_many(self, service, mock_versions_manager):
  371. """Test that only the matching version is returned when multiple exist."""
  372. stable_info = VersionInfo(
  373. remote_info=RemoteInfo(name="ardupilot", url="https://github.com/ArduPilot/ardupilot.git"),
  374. commit_ref="refs/tags/Copter-4.5.0",
  375. release_type="stable",
  376. version_number="4.5.0",
  377. )
  378. beta_info = VersionInfo(
  379. remote_info=RemoteInfo(name="ardupilot", url="https://github.com/ArduPilot/ardupilot.git"),
  380. commit_ref="refs/tags/Copter-4.6.0-beta",
  381. release_type="beta",
  382. version_number="4.6.0",
  383. )
  384. latest_info = VersionInfo(
  385. remote_info=RemoteInfo(name="ardupilot", url="https://github.com/ArduPilot/ardupilot.git"),
  386. commit_ref="refs/heads/master",
  387. release_type="latest",
  388. version_number="NA",
  389. )
  390. mock_versions_manager.get_versions_for_vehicle.return_value = [
  391. stable_info, beta_info, latest_info,
  392. ]
  393. result = service.get_version("copter", beta_info.version_id)
  394. assert result is not None
  395. assert result.id == beta_info.version_id
  396. assert result.type == "beta"
  397. # Tests for get_boards
  398. def test_get_boards_version_not_found_returns_empty(self, service, mock_versions_manager):
  399. """Test that an empty list is returned when the version does not exist."""
  400. mock_versions_manager.get_version_info.return_value = None
  401. result = service.get_boards("copter", "nonexistent-version-id")
  402. assert result == []
  403. def test_get_boards_version_info_queried_with_correct_params(
  404. self, service, mock_versions_manager
  405. ):
  406. """Test that get_version_info is called with the correct vehicle and version IDs."""
  407. mock_versions_manager.get_version_info.return_value = None
  408. service.get_boards("copter", "some-version-id")
  409. mock_versions_manager.get_version_info.assert_called_once_with(
  410. vehicle_id="copter",
  411. version_id="some-version-id",
  412. )
  413. def test_get_boards_empty(
  414. self, service, mock_versions_manager, mock_ap_src_metadata_fetcher
  415. ):
  416. """Test that an empty list is returned when there are no boards for a version."""
  417. version_info = VersionInfo(
  418. remote_info=RemoteInfo(name="ardupilot", url="https://github.com/ArduPilot/ardupilot.git"),
  419. commit_ref="refs/tags/Copter-4.5.0",
  420. release_type="stable",
  421. version_number="4.5.0",
  422. )
  423. mock_versions_manager.get_version_info.return_value = version_info
  424. mock_ap_src_metadata_fetcher.get_boards.return_value = []
  425. result = service.get_boards("copter", version_info.version_id)
  426. assert result == []
  427. def test_get_boards_single(
  428. self, service, mock_versions_manager, mock_ap_src_metadata_fetcher
  429. ):
  430. """Test that a single board is returned correctly."""
  431. version_info = VersionInfo(
  432. remote_info=RemoteInfo(name="ardupilot", url="https://github.com/ArduPilot/ardupilot.git"),
  433. commit_ref="refs/tags/Copter-4.5.0",
  434. release_type="stable",
  435. version_number="4.5.0",
  436. )
  437. mock_versions_manager.get_version_info.return_value = version_info
  438. mock_ap_src_metadata_fetcher.get_boards.return_value = ["CubeRed"]
  439. result = service.get_boards("copter", version_info.version_id)
  440. assert len(result) == 1
  441. assert result[0].id == "CubeRed"
  442. assert result[0].name == "CubeRed"
  443. def test_get_boards_many(
  444. self, service, mock_versions_manager, mock_ap_src_metadata_fetcher
  445. ):
  446. """Test that multiple boards are returned correctly."""
  447. version_info = VersionInfo(
  448. remote_info=RemoteInfo(name="ardupilot", url="https://github.com/ArduPilot/ardupilot.git"),
  449. commit_ref="refs/tags/Copter-4.5.0",
  450. release_type="stable",
  451. version_number="4.5.0",
  452. )
  453. mock_versions_manager.get_version_info.return_value = version_info
  454. mock_ap_src_metadata_fetcher.get_boards.return_value = [
  455. "CubeRed", "CubeOrange", "MatekF405",
  456. ]
  457. result = service.get_boards("copter", version_info.version_id)
  458. assert len(result) == 3
  459. assert [b.id for b in result] == ["CubeRed", "CubeOrange", "MatekF405"]
  460. def test_get_boards_sets_correct_vehicle_and_version_ids(
  461. self, service, mock_versions_manager, mock_ap_src_metadata_fetcher
  462. ):
  463. """Test that returned boards carry the correct vehicle_id and version_id."""
  464. version_info = VersionInfo(
  465. remote_info=RemoteInfo(name="ardupilot", url="https://github.com/ArduPilot/ardupilot.git"),
  466. commit_ref="refs/tags/Copter-4.5.0",
  467. release_type="stable",
  468. version_number="4.5.0",
  469. )
  470. mock_versions_manager.get_version_info.return_value = version_info
  471. mock_ap_src_metadata_fetcher.get_boards.return_value = ["CubeRed"]
  472. result = service.get_boards("copter", version_info.version_id)
  473. assert result[0].vehicle_id == "copter"
  474. assert result[0].version_id == version_info.version_id
  475. def test_get_boards_fetcher_called_with_correct_params(
  476. self, service, mock_versions_manager, mock_ap_src_metadata_fetcher
  477. ):
  478. """Test that the metadata fetcher is called with remote name, commit ref, and vehicle ID from version info."""
  479. version_info = VersionInfo(
  480. remote_info=RemoteInfo(name="ardupilot", url="https://github.com/ArduPilot/ardupilot.git"),
  481. commit_ref="refs/tags/Copter-4.5.0",
  482. release_type="stable",
  483. version_number="4.5.0",
  484. )
  485. mock_versions_manager.get_version_info.return_value = version_info
  486. mock_ap_src_metadata_fetcher.get_boards.return_value = []
  487. service.get_boards("copter", version_info.version_id)
  488. mock_ap_src_metadata_fetcher.get_boards.assert_called_once_with(
  489. remote="ardupilot",
  490. commit_ref="refs/tags/Copter-4.5.0",
  491. vehicle_id="copter",
  492. )
  493. # Tests for get_board
  494. def test_get_board_found(self, service, mock_versions_manager, mock_ap_src_metadata_fetcher):
  495. """Test that the correct board is returned when it exists."""
  496. version_info = VersionInfo(
  497. remote_info=RemoteInfo(name="ardupilot", url="https://github.com/ArduPilot/ardupilot.git"),
  498. commit_ref="refs/tags/Copter-4.5.0",
  499. release_type="stable",
  500. version_number="4.5.0",
  501. )
  502. mock_versions_manager.get_version_info.return_value = version_info
  503. mock_ap_src_metadata_fetcher.get_boards.return_value = ["CubeRed", "CubeOrange"]
  504. result = service.get_board("copter", version_info.version_id, "CubeRed")
  505. assert result is not None
  506. assert result.id == "CubeRed"
  507. assert result.name == "CubeRed"
  508. assert result.vehicle_id == "copter"
  509. assert result.version_id == version_info.version_id
  510. def test_get_board_not_found(self, service, mock_versions_manager, mock_ap_src_metadata_fetcher):
  511. """Test that None is returned when the board does not exist."""
  512. version_info = VersionInfo(
  513. remote_info=RemoteInfo(name="ardupilot", url="https://github.com/ArduPilot/ardupilot.git"),
  514. commit_ref="refs/tags/Copter-4.5.0",
  515. release_type="stable",
  516. version_number="4.5.0",
  517. )
  518. mock_versions_manager.get_version_info.return_value = version_info
  519. mock_ap_src_metadata_fetcher.get_boards.return_value = ["CubeRed", "CubeOrange"]
  520. result = service.get_board("copter", version_info.version_id, "NonExistentBoard")
  521. assert result is None
  522. def test_get_board_returns_correct_match_among_many(
  523. self, service, mock_versions_manager, mock_ap_src_metadata_fetcher
  524. ):
  525. """Test that only the matching board is returned when multiple boards exist."""
  526. version_info = VersionInfo(
  527. remote_info=RemoteInfo(name="ardupilot", url="https://github.com/ArduPilot/ardupilot.git"),
  528. commit_ref="refs/tags/Copter-4.5.0",
  529. release_type="stable",
  530. version_number="4.5.0",
  531. )
  532. mock_versions_manager.get_version_info.return_value = version_info
  533. mock_ap_src_metadata_fetcher.get_boards.return_value = [
  534. "CubeRed", "CubeOrange", "MatekF405",
  535. ]
  536. result = service.get_board("copter", version_info.version_id, "CubeOrange")
  537. assert result is not None
  538. assert result.id == "CubeOrange"
  539. # Tests for get_features
  540. def test_get_features_version_not_found_returns_empty(
  541. self, service, mock_versions_manager
  542. ):
  543. """Test that an empty list is returned when the version does not exist."""
  544. mock_versions_manager.get_version_info.return_value = None
  545. result = service.get_features("copter", "nonexistent-version-id", "CubeRed")
  546. assert result == []
  547. def test_get_features_zero_options_returns_empty(
  548. self, service, mock_versions_manager, mock_ap_src_metadata_fetcher
  549. ):
  550. """Test that an empty list is returned when there are no build options."""
  551. version_info = VersionInfo(
  552. remote_info=RemoteInfo(name="ardupilot", url="https://github.com/ArduPilot/ardupilot.git"),
  553. commit_ref="refs/tags/Copter-4.5.0",
  554. release_type="stable",
  555. version_number="4.5.0",
  556. )
  557. mock_versions_manager.get_version_info.return_value = version_info
  558. mock_ap_src_metadata_fetcher.get_build_options_at_commit.return_value = []
  559. result = service.get_features("copter", version_info.version_id, "CubeRed")
  560. assert result == []
  561. def test_get_features_one_option(
  562. self, service, mock_versions_manager, mock_ap_src_metadata_fetcher
  563. ):
  564. """Test that a single feature is returned correctly."""
  565. version_info = VersionInfo(
  566. remote_info=RemoteInfo(name="ardupilot", url="https://github.com/ArduPilot/ardupilot.git"),
  567. commit_ref="refs/tags/Copter-4.5.0",
  568. release_type="stable",
  569. version_number="4.5.0",
  570. )
  571. opt = Mock()
  572. opt.label = "HAL_LOGGING_ENABLED"
  573. opt.define = "HAL_LOGGING_ENABLED"
  574. opt.category = "Logging"
  575. opt.description = ""
  576. opt.default = 1
  577. opt.dependency = None
  578. mock_versions_manager.get_version_info.return_value = version_info
  579. mock_ap_src_metadata_fetcher.get_build_options_at_commit.return_value = [opt]
  580. result = service.get_features("copter", version_info.version_id, "CubeRed")
  581. assert len(result) == 1
  582. assert result[0].id == "HAL_LOGGING_ENABLED"
  583. assert result[0].name == "HAL_LOGGING_ENABLED"
  584. def test_get_features_many_options(
  585. self, service, mock_versions_manager, mock_ap_src_metadata_fetcher
  586. ):
  587. """Test that all features are returned when multiple options exist."""
  588. version_info = VersionInfo(
  589. remote_info=RemoteInfo(name="ardupilot", url="https://github.com/ArduPilot/ardupilot.git"),
  590. commit_ref="refs/tags/Copter-4.5.0",
  591. release_type="stable",
  592. version_number="4.5.0",
  593. )
  594. opt_logging = Mock()
  595. opt_logging.label, opt_logging.define, opt_logging.category = "HAL_LOGGING_ENABLED", "HAL_LOGGING_ENABLED", "Logging"
  596. opt_logging.description, opt_logging.default, opt_logging.dependency = "", 1, None
  597. opt_ekf = Mock()
  598. opt_ekf.label, opt_ekf.define, opt_ekf.category = "HAL_NAVEKF3_AVAILABLE", "HAL_NAVEKF3_AVAILABLE", "EKF"
  599. opt_ekf.description, opt_ekf.default, opt_ekf.dependency = "", 1, None
  600. opt_sensors = Mock()
  601. opt_sensors.label, opt_sensors.define, opt_sensors.category = "HAL_BEACON_ENABLED", "HAL_BEACON_ENABLED", "Sensors"
  602. opt_sensors.description, opt_sensors.default, opt_sensors.dependency = "", 1, None
  603. mock_versions_manager.get_version_info.return_value = version_info
  604. mock_ap_src_metadata_fetcher.get_build_options_at_commit.return_value = [opt_logging, opt_ekf, opt_sensors]
  605. result = service.get_features("copter", version_info.version_id, "CubeRed")
  606. assert len(result) == 3
  607. def test_get_features_sorted_by_category(
  608. self, service, mock_versions_manager, mock_ap_src_metadata_fetcher
  609. ):
  610. """Test that features are sorted by category name."""
  611. version_info = VersionInfo(
  612. remote_info=RemoteInfo(name="ardupilot", url="https://github.com/ArduPilot/ardupilot.git"),
  613. commit_ref="refs/tags/Copter-4.5.0",
  614. release_type="stable",
  615. version_number="4.5.0",
  616. )
  617. opt_z = Mock()
  618. opt_z.label, opt_z.define, opt_z.category = "FEATURE_Z", "DEFINE_Z", "Sensors"
  619. opt_z.description, opt_z.default, opt_z.dependency = "", 1, None
  620. opt_a = Mock()
  621. opt_a.label, opt_a.define, opt_a.category = "FEATURE_A", "DEFINE_A", "EKF"
  622. opt_a.description, opt_a.default, opt_a.dependency = "", 1, None
  623. opt_m = Mock()
  624. opt_m.label, opt_m.define, opt_m.category = "FEATURE_M", "DEFINE_M", "Logging"
  625. opt_m.description, opt_m.default, opt_m.dependency = "", 1, None
  626. mock_versions_manager.get_version_info.return_value = version_info
  627. mock_ap_src_metadata_fetcher.get_build_options_at_commit.return_value = [opt_z, opt_a, opt_m]
  628. result = service.get_features("copter", version_info.version_id, "CubeRed")
  629. assert [f.category.name for f in result] == ["EKF", "Logging", "Sensors"]
  630. def test_get_features_uses_fallback_defaults_when_no_features_url(
  631. self, service, mock_versions_manager, mock_ap_src_metadata_fetcher,
  632. mock_manifest_json, mock_features_txt_client,
  633. ):
  634. """Test that build-options-py defaults are used when no features.txt URL is available."""
  635. version_info = VersionInfo(
  636. remote_info=RemoteInfo(name="ardupilot", url="https://github.com/ArduPilot/ardupilot.git"),
  637. commit_ref="refs/tags/Copter-4.5.0",
  638. release_type="stable",
  639. version_number="4.5.0",
  640. )
  641. opt_on = Mock()
  642. opt_on.label, opt_on.define, opt_on.category = "FEATURE_ON", "DEFINE_ON", "Cat"
  643. opt_on.description, opt_on.default, opt_on.dependency = "", 1, None
  644. opt_off = Mock()
  645. opt_off.label, opt_off.define, opt_off.category = "FEATURE_OFF", "DEFINE_OFF", "Cat"
  646. opt_off.description, opt_off.default, opt_off.dependency = "", 0, None
  647. mock_versions_manager.get_version_info.return_value = version_info
  648. mock_ap_src_metadata_fetcher.get_build_options_at_commit.return_value = [opt_on, opt_off]
  649. mock_manifest_json.get_features_txt_url.return_value = None
  650. result = service.get_features("copter", version_info.version_id, "CubeRed")
  651. by_id = {f.id: f.default for f in result}
  652. assert by_id["FEATURE_ON"].enabled is True
  653. assert by_id["FEATURE_ON"].source == "build-options-py"
  654. assert by_id["FEATURE_OFF"].enabled is False
  655. assert by_id["FEATURE_OFF"].source == "build-options-py"
  656. mock_features_txt_client.get_defaults.assert_not_called()
  657. def test_get_features_uses_firmware_server_defaults_when_available(
  658. self, service, mock_versions_manager, mock_ap_src_metadata_fetcher,
  659. mock_manifest_json, mock_features_txt_client,
  660. ):
  661. """Test that firmware-server defaults override build-options-py when present."""
  662. version_info = VersionInfo(
  663. remote_info=RemoteInfo(name="ardupilot", url="https://github.com/ArduPilot/ardupilot.git"),
  664. commit_ref="refs/tags/Copter-4.5.0",
  665. release_type="stable",
  666. version_number="4.5.0",
  667. )
  668. opt_a = Mock()
  669. opt_a.label, opt_a.define, opt_a.category = "FEATURE_A", "DEFINE_A", "Cat"
  670. opt_a.description, opt_a.default, opt_a.dependency = "", 1, None
  671. opt_b = Mock()
  672. opt_b.label, opt_b.define, opt_b.category = "FEATURE_B", "DEFINE_B", "Cat"
  673. opt_b.description, opt_b.default, opt_b.dependency = "", 1, None
  674. mock_versions_manager.get_version_info.return_value = version_info
  675. mock_ap_src_metadata_fetcher.get_build_options_at_commit.return_value = [opt_a, opt_b]
  676. features_url = (
  677. "https://firmware.ardupilot.org/Copter/stable-4.5.0/CubeRed/features.txt"
  678. )
  679. mock_manifest_json.get_features_txt_url.return_value = features_url
  680. mock_features_txt_client.get_defaults.return_value = {
  681. "DEFINE_A": 0,
  682. "DEFINE_B": 1,
  683. }
  684. result = service.get_features("copter", version_info.version_id, "CubeRed")
  685. by_id = {f.id: f.default for f in result}
  686. assert by_id["FEATURE_A"].enabled is False
  687. assert by_id["FEATURE_A"].source == "firmware-server"
  688. assert by_id["FEATURE_B"].enabled is True
  689. assert by_id["FEATURE_B"].source == "firmware-server"
  690. mock_features_txt_client.get_defaults.assert_called_once_with(features_url)
  691. def test_get_features_falls_back_to_defaults_when_firmware_server_returns_none(
  692. self, service, mock_versions_manager, mock_ap_src_metadata_fetcher,
  693. mock_manifest_json, mock_features_txt_client,
  694. ):
  695. """Test that build-options-py fallback is used when firmware server fetch fails."""
  696. version_info = VersionInfo(
  697. remote_info=RemoteInfo(name="ardupilot", url="https://github.com/ArduPilot/ardupilot.git"),
  698. commit_ref="refs/tags/Copter-4.5.0",
  699. release_type="stable",
  700. version_number="4.5.0",
  701. )
  702. opt = Mock()
  703. opt.label, opt.define, opt.category = "FEATURE_A", "DEFINE_A", "Cat"
  704. opt.description, opt.default, opt.dependency = "", 1, None
  705. mock_versions_manager.get_version_info.return_value = version_info
  706. mock_ap_src_metadata_fetcher.get_build_options_at_commit.return_value = [opt]
  707. mock_manifest_json.get_features_txt_url.return_value = (
  708. "https://firmware.ardupilot.org/Copter/stable-4.5.0/CubeRed/features.txt"
  709. )
  710. mock_features_txt_client.get_defaults.return_value = None
  711. result = service.get_features("copter", version_info.version_id, "CubeRed")
  712. assert result[0].default.enabled is True
  713. assert result[0].default.source == "build-options-py"
  714. def test_get_features_firmware_server_overrides_only_known_defines(
  715. self, service, mock_versions_manager, mock_ap_src_metadata_fetcher,
  716. mock_manifest_json, mock_features_txt_client,
  717. ):
  718. """Test that a define absent from firmware-server data falls back to build-options-py."""
  719. version_info = VersionInfo(
  720. remote_info=RemoteInfo(name="ardupilot", url="https://github.com/ArduPilot/ardupilot.git"),
  721. commit_ref="refs/tags/Copter-4.5.0",
  722. release_type="stable",
  723. version_number="4.5.0",
  724. )
  725. opt_known = Mock()
  726. opt_known.label, opt_known.define, opt_known.category = "FEATURE_KNOWN", "DEFINE_KNOWN", "Cat"
  727. opt_known.description, opt_known.default, opt_known.dependency = "", 0, None
  728. opt_unknown = Mock()
  729. opt_unknown.label, opt_unknown.define, opt_unknown.category = "FEATURE_UNKNOWN", "DEFINE_UNKNOWN", "Cat"
  730. opt_unknown.description, opt_unknown.default, opt_unknown.dependency = "", 1, None
  731. mock_versions_manager.get_version_info.return_value = version_info
  732. mock_ap_src_metadata_fetcher.get_build_options_at_commit.return_value = [opt_known, opt_unknown]
  733. mock_manifest_json.get_features_txt_url.return_value = (
  734. "https://firmware.ardupilot.org/Copter/stable-4.5.0/CubeRed/features.txt"
  735. )
  736. mock_features_txt_client.get_defaults.return_value = {
  737. "DEFINE_KNOWN": 1,
  738. }
  739. result = service.get_features("copter", version_info.version_id, "CubeRed")
  740. by_id = {f.id: f.default for f in result}
  741. assert by_id["FEATURE_KNOWN"].enabled is True
  742. assert by_id["FEATURE_KNOWN"].source == "firmware-server"
  743. assert by_id["FEATURE_UNKNOWN"].enabled is True
  744. assert by_id["FEATURE_UNKNOWN"].source == "build-options-py"
  745. def test_get_features_tag_version_uses_latest_url(
  746. self, service, mock_versions_manager, mock_ap_src_metadata_fetcher,
  747. mock_manifest_json, mock_features_txt_client,
  748. ):
  749. """Fork tag builds use hardcoded latest features.txt URL."""
  750. version_info = VersionInfo(
  751. remote_info=RemoteInfo(name="shiv-tyagi", url="https://github.com/shiv-tyagi/ardupilot.git"),
  752. commit_ref="refs/tags/custom-build/my-feature",
  753. release_type="tag",
  754. version_number="my-feature",
  755. )
  756. opt = Mock()
  757. opt.label, opt.define, opt.category = "FEATURE_A", "DEFINE_A", "Cat"
  758. opt.description, opt.default, opt.dependency = "", 0, None
  759. mock_versions_manager.get_version_info.return_value = version_info
  760. mock_ap_src_metadata_fetcher.get_build_options_at_commit.return_value = [opt]
  761. latest_url = (
  762. "https://firmware.ardupilot.org/Copter/latest/CubeOrange/features.txt"
  763. )
  764. mock_features_txt_client.get_defaults.return_value = {"DEFINE_A": 1}
  765. result = service.get_features("copter", version_info.version_id, "CubeOrange")
  766. mock_manifest_json.get_features_txt_url.assert_not_called()
  767. mock_features_txt_client.get_defaults.assert_called_once_with(latest_url)
  768. assert result[0].default.enabled is True
  769. assert result[0].default.source == "firmware-server"
  770. def test_get_features_fork_remote_uses_latest_url(
  771. self, service, mock_versions_manager, mock_ap_src_metadata_fetcher,
  772. mock_manifest_json, mock_features_txt_client,
  773. ):
  774. """Non-official fork remotes use latest ardupilot features.txt for any release."""
  775. version_info = VersionInfo(
  776. remote_info=RemoteInfo(
  777. name="test-remote-1",
  778. url="https://github.com/test/ardupilot.git",
  779. ),
  780. commit_ref="refs/heads/master",
  781. release_type="latest",
  782. version_number="4.6.0",
  783. )
  784. opt = Mock()
  785. opt.label, opt.define, opt.category = "FEATURE_A", "DEFINE_A", "Cat"
  786. opt.description, opt.default, opt.dependency = "", 1, None
  787. mock_versions_manager.get_version_info.return_value = version_info
  788. mock_ap_src_metadata_fetcher.get_build_options_at_commit.return_value = [opt]
  789. latest_url = (
  790. "https://firmware.ardupilot.org/Copter/latest/CubeOrange/features.txt"
  791. )
  792. mock_features_txt_client.get_defaults.return_value = {"DEFINE_A": 0}
  793. result = service.get_features("copter", version_info.version_id, "CubeOrange")
  794. mock_manifest_json.get_features_txt_url.assert_not_called()
  795. mock_features_txt_client.get_defaults.assert_called_once_with(latest_url)
  796. assert result[0].default.enabled is False
  797. assert result[0].default.source == "firmware-server"
  798. def test_get_features_dependency_none(
  799. self, service, mock_versions_manager, mock_ap_src_metadata_fetcher
  800. ):
  801. """Test that a feature with no dependency produces an empty dependencies list."""
  802. version_info = VersionInfo(
  803. remote_info=RemoteInfo(name="ardupilot", url="https://github.com/ArduPilot/ardupilot.git"),
  804. commit_ref="refs/tags/Copter-4.5.0",
  805. release_type="stable",
  806. version_number="4.5.0",
  807. )
  808. opt = Mock()
  809. opt.label, opt.define, opt.category = "FEATURE_A", "DEFINE_A", "Cat"
  810. opt.description, opt.default, opt.dependency = "", 1, None
  811. mock_versions_manager.get_version_info.return_value = version_info
  812. mock_ap_src_metadata_fetcher.get_build_options_at_commit.return_value = [opt]
  813. result = service.get_features("copter", version_info.version_id, "CubeRed")
  814. assert result[0].dependencies == []
  815. def test_get_features_dependency_single(
  816. self, service, mock_versions_manager, mock_ap_src_metadata_fetcher
  817. ):
  818. """Test that a single dependency string is parsed into a one-element list."""
  819. version_info = VersionInfo(
  820. remote_info=RemoteInfo(name="ardupilot", url="https://github.com/ArduPilot/ardupilot.git"),
  821. commit_ref="refs/tags/Copter-4.5.0",
  822. release_type="stable",
  823. version_number="4.5.0",
  824. )
  825. opt = Mock()
  826. opt.label, opt.define, opt.category = "FEATURE_A", "DEFINE_A", "Cat"
  827. opt.description, opt.default, opt.dependency = "", 1, "DEP_ONE"
  828. mock_versions_manager.get_version_info.return_value = version_info
  829. mock_ap_src_metadata_fetcher.get_build_options_at_commit.return_value = [opt]
  830. result = service.get_features("copter", version_info.version_id, "CubeRed")
  831. assert result[0].dependencies == ["DEP_ONE"]
  832. def test_get_features_dependency_multiple_comma_separated(
  833. self, service, mock_versions_manager, mock_ap_src_metadata_fetcher
  834. ):
  835. """Test that a comma-separated dependency string is split into multiple entries."""
  836. version_info = VersionInfo(
  837. remote_info=RemoteInfo(name="ardupilot", url="https://github.com/ArduPilot/ardupilot.git"),
  838. commit_ref="refs/tags/Copter-4.5.0",
  839. release_type="stable",
  840. version_number="4.5.0",
  841. )
  842. opt = Mock()
  843. opt.label, opt.define, opt.category = "FEATURE_A", "DEFINE_A", "Cat"
  844. opt.description, opt.default, opt.dependency = "", 1, "DEP_ONE,DEP_TWO,DEP_THREE"
  845. mock_versions_manager.get_version_info.return_value = version_info
  846. mock_ap_src_metadata_fetcher.get_build_options_at_commit.return_value = [opt]
  847. result = service.get_features("copter", version_info.version_id, "CubeRed")
  848. assert result[0].dependencies == ["DEP_ONE", "DEP_TWO", "DEP_THREE"]
  849. def test_get_features_dependency_with_spaces(
  850. self, service, mock_versions_manager, mock_ap_src_metadata_fetcher
  851. ):
  852. """Test that spaces around dependency labels are stripped."""
  853. version_info = VersionInfo(
  854. remote_info=RemoteInfo(name="ardupilot", url="https://github.com/ArduPilot/ardupilot.git"),
  855. commit_ref="refs/tags/Copter-4.5.0",
  856. release_type="stable",
  857. version_number="4.5.0",
  858. )
  859. opt = Mock()
  860. opt.label, opt.define, opt.category = "FEATURE_A", "DEFINE_A", "Cat"
  861. opt.description, opt.default, opt.dependency = "", 1, "DEP_ONE , DEP_TWO , DEP_THREE"
  862. mock_versions_manager.get_version_info.return_value = version_info
  863. mock_ap_src_metadata_fetcher.get_build_options_at_commit.return_value = [opt]
  864. result = service.get_features("copter", version_info.version_id, "CubeRed")
  865. assert result[0].dependencies == ["DEP_ONE", "DEP_TWO", "DEP_THREE"]
  866. def test_get_features_ids_filled_correctly(
  867. self, service, mock_versions_manager, mock_ap_src_metadata_fetcher
  868. ):
  869. """Test that vehicle_id, version_id, and board_id are correctly set on each feature."""
  870. version_info = VersionInfo(
  871. remote_info=RemoteInfo(name="ardupilot", url="https://github.com/ArduPilot/ardupilot.git"),
  872. commit_ref="refs/tags/Copter-4.5.0",
  873. release_type="stable",
  874. version_number="4.5.0",
  875. )
  876. opt = Mock()
  877. opt.label, opt.define, opt.category = "FEATURE_A", "DEFINE_A", "Cat"
  878. opt.description, opt.default, opt.dependency = "", 1, None
  879. mock_versions_manager.get_version_info.return_value = version_info
  880. mock_ap_src_metadata_fetcher.get_build_options_at_commit.return_value = [opt]
  881. result = service.get_features("copter", version_info.version_id, "CubeRed")
  882. assert result[0].vehicle_id == "copter"
  883. assert result[0].version_id == version_info.version_id
  884. assert result[0].board_id == "CubeRed"
  885. def test_get_features_category_filter_keeps_matching(
  886. self, service, mock_versions_manager, mock_ap_src_metadata_fetcher
  887. ):
  888. """Test that category_id filter returns only features whose category matches."""
  889. version_info = VersionInfo(
  890. remote_info=RemoteInfo(name="ardupilot", url="https://github.com/ArduPilot/ardupilot.git"),
  891. commit_ref="refs/tags/Copter-4.5.0",
  892. release_type="stable",
  893. version_number="4.5.0",
  894. )
  895. opt_logging = Mock()
  896. opt_logging.label, opt_logging.define, opt_logging.category = "HAL_LOGGING_ENABLED", "HAL_LOGGING_ENABLED", "Logging"
  897. opt_logging.description, opt_logging.default, opt_logging.dependency = "", 1, None
  898. opt_ekf = Mock()
  899. opt_ekf.label, opt_ekf.define, opt_ekf.category = "HAL_NAVEKF3_AVAILABLE", "HAL_NAVEKF3_AVAILABLE", "EKF"
  900. opt_ekf.description, opt_ekf.default, opt_ekf.dependency = "", 1, None
  901. opt_sensors = Mock()
  902. opt_sensors.label, opt_sensors.define, opt_sensors.category = "HAL_BEACON_ENABLED", "HAL_BEACON_ENABLED", "Sensors"
  903. opt_sensors.description, opt_sensors.default, opt_sensors.dependency = "", 1, None
  904. mock_versions_manager.get_version_info.return_value = version_info
  905. mock_ap_src_metadata_fetcher.get_build_options_at_commit.return_value = [opt_logging, opt_ekf, opt_sensors]
  906. result = service.get_features("copter", version_info.version_id, "CubeRed", category_id="Logging")
  907. assert len(result) == 1
  908. assert result[0].id == "HAL_LOGGING_ENABLED"
  909. assert result[0].category.name == "Logging"
  910. def test_get_features_category_filter_excludes_non_matching(
  911. self, service, mock_versions_manager, mock_ap_src_metadata_fetcher
  912. ):
  913. """Test that category_id filter excludes features whose category does not match."""
  914. version_info = VersionInfo(
  915. remote_info=RemoteInfo(name="ardupilot", url="https://github.com/ArduPilot/ardupilot.git"),
  916. commit_ref="refs/tags/Copter-4.5.0",
  917. release_type="stable",
  918. version_number="4.5.0",
  919. )
  920. opt_logging = Mock()
  921. opt_logging.label, opt_logging.define, opt_logging.category = "HAL_LOGGING_ENABLED", "HAL_LOGGING_ENABLED", "Logging"
  922. opt_logging.description, opt_logging.default, opt_logging.dependency = "", 1, None
  923. opt_ekf = Mock()
  924. opt_ekf.label, opt_ekf.define, opt_ekf.category = "HAL_NAVEKF3_AVAILABLE", "HAL_NAVEKF3_AVAILABLE", "EKF"
  925. opt_ekf.description, opt_ekf.default, opt_ekf.dependency = "", 1, None
  926. mock_versions_manager.get_version_info.return_value = version_info
  927. mock_ap_src_metadata_fetcher.get_build_options_at_commit.return_value = [opt_logging, opt_ekf]
  928. result = service.get_features("copter", version_info.version_id, "CubeRed", category_id="Sensors")
  929. assert result == []
  930. def test_get_features_category_filter_no_matches_returns_empty(
  931. self, service, mock_versions_manager, mock_ap_src_metadata_fetcher
  932. ):
  933. """Test that a category_id with no matching features returns an empty list."""
  934. version_info = VersionInfo(
  935. remote_info=RemoteInfo(name="ardupilot", url="https://github.com/ArduPilot/ardupilot.git"),
  936. commit_ref="refs/tags/Copter-4.5.0",
  937. release_type="stable",
  938. version_number="4.5.0",
  939. )
  940. opt_a = Mock()
  941. opt_a.label, opt_a.define, opt_a.category = "FEATURE_A", "DEFINE_A", "Logging"
  942. opt_a.description, opt_a.default, opt_a.dependency = "", 1, None
  943. opt_b = Mock()
  944. opt_b.label, opt_b.define, opt_b.category = "FEATURE_B", "DEFINE_B", "Logging"
  945. opt_b.description, opt_b.default, opt_b.dependency = "", 1, None
  946. mock_versions_manager.get_version_info.return_value = version_info
  947. mock_ap_src_metadata_fetcher.get_build_options_at_commit.return_value = [opt_a, opt_b]
  948. result = service.get_features("copter", version_info.version_id, "CubeRed", category_id="NonExistent")
  949. assert result == []
  950. # Tests for get_feature
  951. def test_get_feature_found(
  952. self, service, mock_versions_manager, mock_ap_src_metadata_fetcher
  953. ):
  954. """Test that the correct feature is returned when it exists."""
  955. version_info = VersionInfo(
  956. remote_info=RemoteInfo(name="ardupilot", url="https://github.com/ArduPilot/ardupilot.git"),
  957. commit_ref="refs/tags/Copter-4.5.0",
  958. release_type="stable",
  959. version_number="4.5.0",
  960. )
  961. opt = Mock()
  962. opt.label, opt.define, opt.category = "HAL_LOGGING_ENABLED", "HAL_LOGGING_ENABLED", "Logging"
  963. opt.description, opt.default, opt.dependency = "", 1, None
  964. mock_versions_manager.get_version_info.return_value = version_info
  965. mock_ap_src_metadata_fetcher.get_build_options_at_commit.return_value = [opt]
  966. result = service.get_feature("copter", version_info.version_id, "CubeRed", "HAL_LOGGING_ENABLED")
  967. assert result is not None
  968. assert result.id == "HAL_LOGGING_ENABLED"
  969. assert result.name == "HAL_LOGGING_ENABLED"
  970. def test_get_feature_not_found(
  971. self, service, mock_versions_manager, mock_ap_src_metadata_fetcher
  972. ):
  973. """Test that None is returned when the feature does not exist."""
  974. version_info = VersionInfo(
  975. remote_info=RemoteInfo(name="ardupilot", url="https://github.com/ArduPilot/ardupilot.git"),
  976. commit_ref="refs/tags/Copter-4.5.0",
  977. release_type="stable",
  978. version_number="4.5.0",
  979. )
  980. opt = Mock()
  981. opt.label, opt.define, opt.category = "HAL_LOGGING_ENABLED", "HAL_LOGGING_ENABLED", "Logging"
  982. opt.description, opt.default, opt.dependency = "", 1, None
  983. mock_versions_manager.get_version_info.return_value = version_info
  984. mock_ap_src_metadata_fetcher.get_build_options_at_commit.return_value = [opt]
  985. result = service.get_feature("copter", version_info.version_id, "CubeRed", "NONEXISTENT_FEATURE")
  986. assert result is None
  987. def test_get_feature_returns_correct_match_among_many(
  988. self, service, mock_versions_manager, mock_ap_src_metadata_fetcher
  989. ):
  990. """Test that only the matching feature is returned when multiple features exist."""
  991. version_info = VersionInfo(
  992. remote_info=RemoteInfo(name="ardupilot", url="https://github.com/ArduPilot/ardupilot.git"),
  993. commit_ref="refs/tags/Copter-4.5.0",
  994. release_type="stable",
  995. version_number="4.5.0",
  996. )
  997. opt_a = Mock()
  998. opt_a.label, opt_a.define, opt_a.category = "FEATURE_A", "DEFINE_A", "Cat"
  999. opt_a.description, opt_a.default, opt_a.dependency = "", 1, None
  1000. opt_b = Mock()
  1001. opt_b.label, opt_b.define, opt_b.category = "FEATURE_B", "DEFINE_B", "Cat"
  1002. opt_b.description, opt_b.default, opt_b.dependency = "", 0, None
  1003. opt_c = Mock()
  1004. opt_c.label, opt_c.define, opt_c.category = "FEATURE_C", "DEFINE_C", "Cat"
  1005. opt_c.description, opt_c.default, opt_c.dependency = "", 1, None
  1006. mock_versions_manager.get_version_info.return_value = version_info
  1007. mock_ap_src_metadata_fetcher.get_build_options_at_commit.return_value = [opt_a, opt_b, opt_c]
  1008. result = service.get_feature("copter", version_info.version_id, "CubeRed", "FEATURE_B")
  1009. assert result is not None
  1010. assert result.id == "FEATURE_B"
  1011. assert result.default.enabled is False
  1012. class TestGetBoardStandardArtifacts:
  1013. def test_board_not_found_returns_none(
  1014. self, service, mock_versions_manager, mock_ap_src_metadata_fetcher,
  1015. mock_manifest_json,
  1016. ):
  1017. remote_info = Mock()
  1018. remote_info.name = "ardupilot"
  1019. mock_versions_manager.get_version_info.return_value = Mock(
  1020. version_id="v1",
  1021. commit_ref="abc123",
  1022. remote_info=remote_info,
  1023. )
  1024. mock_ap_src_metadata_fetcher.get_boards.return_value = []
  1025. result = service.get_board_standard_artifacts("copter", "v1", "CubeOrange")
  1026. assert result is None
  1027. mock_manifest_json.get_board_artifacts.assert_not_called()
  1028. def test_success(
  1029. self, service, mock_versions_manager, mock_ap_src_metadata_fetcher,
  1030. mock_manifest_json,
  1031. ):
  1032. remote_info = Mock()
  1033. remote_info.name = "ardupilot"
  1034. version_info = Mock(
  1035. version_id="v1",
  1036. commit_ref="abc123",
  1037. release_type="stable",
  1038. version_number="4.6.3",
  1039. remote_info=remote_info,
  1040. )
  1041. mock_versions_manager.get_version_info.return_value = version_info
  1042. mock_ap_src_metadata_fetcher.get_boards.return_value = ["CubeOrange"]
  1043. mock_manifest_json.get_board_artifacts.return_value = [
  1044. BoardArtifact(
  1045. name="arducopter.apj",
  1046. url="https://firmware.ardupilot.org/Copter/stable-4.6.3/CubeOrange/arducopter.apj",
  1047. format="apj",
  1048. size=100,
  1049. )
  1050. ]
  1051. result = service.get_board_standard_artifacts("copter", "v1", "CubeOrange")
  1052. assert len(result) == 1
  1053. assert result[0].name == "arducopter.apj"
  1054. assert result[0].format == "apj"
  1055. mock_manifest_json.get_board_artifacts.assert_called_once_with(
  1056. vehicle_id="copter",
  1057. release_type="stable",
  1058. version_number="4.6.3",
  1059. board_id="CubeOrange",
  1060. )
  1061. def test_not_in_manifest_returns_none(
  1062. self, service, mock_versions_manager, mock_ap_src_metadata_fetcher,
  1063. mock_manifest_json,
  1064. ):
  1065. remote_info = Mock()
  1066. remote_info.name = "ardupilot"
  1067. version_info = Mock(
  1068. version_id="v1",
  1069. commit_ref="abc123",
  1070. release_type="stable",
  1071. version_number="4.6.3",
  1072. remote_info=remote_info,
  1073. )
  1074. mock_versions_manager.get_version_info.return_value = version_info
  1075. mock_ap_src_metadata_fetcher.get_boards.return_value = ["CubeOrange"]
  1076. mock_manifest_json.get_board_artifacts.return_value = []
  1077. result = service.get_board_standard_artifacts("copter", "v1", "CubeOrange")
  1078. assert result is None
  1079. def test_fork_remote_returns_none_without_manifest_lookup(
  1080. self, service, mock_versions_manager, mock_ap_src_metadata_fetcher,
  1081. mock_manifest_json,
  1082. ):
  1083. remote_info = Mock()
  1084. remote_info.name = "shiv-tyagi"
  1085. version_info = Mock(
  1086. version_id="v1",
  1087. commit_ref="abc123",
  1088. release_type="tag",
  1089. version_number="my-feature",
  1090. remote_info=remote_info,
  1091. )
  1092. mock_versions_manager.get_version_info.return_value = version_info
  1093. mock_ap_src_metadata_fetcher.get_boards.return_value = ["CubeOrange"]
  1094. result = service.get_board_standard_artifacts("copter", "v1", "CubeOrange")
  1095. assert result is None
  1096. mock_manifest_json.get_board_artifacts.assert_not_called()