asset.py 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177
  1. # Test utilities for fetching & caching assets
  2. #
  3. # Copyright 2024 Red Hat, Inc.
  4. #
  5. # This work is licensed under the terms of the GNU GPL, version 2 or
  6. # later. See the COPYING file in the top-level directory.
  7. import hashlib
  8. import logging
  9. import os
  10. import subprocess
  11. import sys
  12. import unittest
  13. import urllib.request
  14. from time import sleep
  15. from pathlib import Path
  16. from shutil import copyfileobj
  17. # Instances of this class must be declared as class level variables
  18. # starting with a name "ASSET_". This enables the pre-caching logic
  19. # to easily find all referenced assets and download them prior to
  20. # execution of the tests.
  21. class Asset:
  22. def __init__(self, url, hashsum):
  23. self.url = url
  24. self.hash = hashsum
  25. cache_dir_env = os.getenv('QEMU_TEST_CACHE_DIR')
  26. if cache_dir_env:
  27. self.cache_dir = Path(cache_dir_env, "download")
  28. else:
  29. self.cache_dir = Path(Path("~").expanduser(),
  30. ".cache", "qemu", "download")
  31. self.cache_file = Path(self.cache_dir, hashsum)
  32. self.log = logging.getLogger('qemu-test')
  33. def __repr__(self):
  34. return "Asset: url=%s hash=%s cache=%s" % (
  35. self.url, self.hash, self.cache_file)
  36. def _check(self, cache_file):
  37. if self.hash is None:
  38. return True
  39. if len(self.hash) == 64:
  40. hl = hashlib.sha256()
  41. elif len(self.hash) == 128:
  42. hl = hashlib.sha512()
  43. else:
  44. raise Exception("unknown hash type")
  45. # Calculate the hash of the file:
  46. with open(cache_file, 'rb') as file:
  47. while True:
  48. chunk = file.read(1 << 20)
  49. if not chunk:
  50. break
  51. hl.update(chunk)
  52. return self.hash == hl.hexdigest()
  53. def valid(self):
  54. return self.cache_file.exists() and self._check(self.cache_file)
  55. def _wait_for_other_download(self, tmp_cache_file):
  56. # Another thread already seems to download the asset, so wait until
  57. # it is done, while also checking the size to see whether it is stuck
  58. try:
  59. current_size = tmp_cache_file.stat().st_size
  60. new_size = current_size
  61. except:
  62. if os.path.exists(self.cache_file):
  63. return True
  64. raise
  65. waittime = lastchange = 600
  66. while waittime > 0:
  67. sleep(1)
  68. waittime -= 1
  69. try:
  70. new_size = tmp_cache_file.stat().st_size
  71. except:
  72. if os.path.exists(self.cache_file):
  73. return True
  74. raise
  75. if new_size != current_size:
  76. lastchange = waittime
  77. current_size = new_size
  78. elif lastchange - waittime > 90:
  79. return False
  80. self.log.debug("Time out while waiting for %s!", tmp_cache_file)
  81. raise
  82. def fetch(self):
  83. if not self.cache_dir.exists():
  84. self.cache_dir.mkdir(parents=True, exist_ok=True)
  85. if self.valid():
  86. self.log.debug("Using cached asset %s for %s",
  87. self.cache_file, self.url)
  88. return str(self.cache_file)
  89. if os.environ.get("QEMU_TEST_NO_DOWNLOAD", False):
  90. raise Exception("Asset cache is invalid and downloads disabled")
  91. self.log.info("Downloading %s to %s...", self.url, self.cache_file)
  92. tmp_cache_file = self.cache_file.with_suffix(".download")
  93. for retries in range(3):
  94. try:
  95. with tmp_cache_file.open("xb") as dst:
  96. with urllib.request.urlopen(self.url) as resp:
  97. copyfileobj(resp, dst)
  98. break
  99. except FileExistsError:
  100. self.log.debug("%s already exists, "
  101. "waiting for other thread to finish...",
  102. tmp_cache_file)
  103. if self._wait_for_other_download(tmp_cache_file):
  104. return str(self.cache_file)
  105. self.log.debug("%s seems to be stale, "
  106. "deleting and retrying download...",
  107. tmp_cache_file)
  108. tmp_cache_file.unlink()
  109. continue
  110. except Exception as e:
  111. self.log.error("Unable to download %s: %s", self.url, e)
  112. tmp_cache_file.unlink()
  113. raise
  114. try:
  115. # Set these just for informational purposes
  116. os.setxattr(str(tmp_cache_file), "user.qemu-asset-url",
  117. self.url.encode('utf8'))
  118. os.setxattr(str(tmp_cache_file), "user.qemu-asset-hash",
  119. self.hash.encode('utf8'))
  120. except Exception as e:
  121. self.log.debug("Unable to set xattr on %s: %s", tmp_cache_file, e)
  122. pass
  123. if not self._check(tmp_cache_file):
  124. tmp_cache_file.unlink()
  125. raise Exception("Hash of %s does not match %s" %
  126. (self.url, self.hash))
  127. tmp_cache_file.replace(self.cache_file)
  128. self.log.info("Cached %s at %s" % (self.url, self.cache_file))
  129. return str(self.cache_file)
  130. def precache_test(test):
  131. log = logging.getLogger('qemu-test')
  132. log.setLevel(logging.DEBUG)
  133. handler = logging.StreamHandler(sys.stdout)
  134. handler.setLevel(logging.DEBUG)
  135. formatter = logging.Formatter(
  136. '%(asctime)s - %(name)s - %(levelname)s - %(message)s')
  137. handler.setFormatter(formatter)
  138. log.addHandler(handler)
  139. for name, asset in vars(test.__class__).items():
  140. if name.startswith("ASSET_") and type(asset) == Asset:
  141. log.info("Attempting to cache '%s'" % asset)
  142. asset.fetch()
  143. log.removeHandler(handler)
  144. def precache_suite(suite):
  145. for test in suite:
  146. if isinstance(test, unittest.TestSuite):
  147. Asset.precache_suite(test)
  148. elif isinstance(test, unittest.TestCase):
  149. Asset.precache_test(test)
  150. def precache_suites(path, cacheTstamp):
  151. loader = unittest.loader.defaultTestLoader
  152. tests = loader.loadTestsFromNames([path], None)
  153. with open(cacheTstamp, "w") as fh:
  154. Asset.precache_suite(tests)