parent
							
								
									bc23cf8038
								
							
						
					
					
						commit
						0d895ad82a
					
				
				 5 changed files with 317 additions and 107 deletions
			
			
		| @ -0,0 +1,124 @@ | |||||||
|  | #!/usr/bin/env python3 | ||||||
|  | import math | ||||||
|  | import os | ||||||
|  | import random | ||||||
|  | import shutil | ||||||
|  | import subprocess | ||||||
|  | import time | ||||||
|  | import unittest | ||||||
|  | from parameterized import parameterized | ||||||
|  | from pathlib import Path | ||||||
|  | from tqdm import trange | ||||||
|  | 
 | ||||||
|  | from common.params import Params | ||||||
|  | from common.timeout import Timeout | ||||||
|  | from selfdrive.hardware import EON, TICI | ||||||
|  | from selfdrive.test.helpers import with_processes | ||||||
|  | from selfdrive.loggerd.config import ROOT, CAMERA_FPS | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | # baseline file sizes for a 2s segment, in bytes | ||||||
|  | FULL_SIZE = 1253786 | ||||||
|  | if EON: | ||||||
|  |   CAMERAS = { | ||||||
|  |     "fcamera": FULL_SIZE, | ||||||
|  |     "dcamera": 770920, | ||||||
|  |     "qcamera": 38533, | ||||||
|  |   } | ||||||
|  | elif TICI: | ||||||
|  |   CAMERAS = {f"{c}camera": FULL_SIZE if c!="q" else 38533 for c in ["f", "e", "d", "q"]} | ||||||
|  | else: | ||||||
|  |   CAMERAS = {} | ||||||
|  | 
 | ||||||
|  | ALL_CAMERA_COMBINATIONS = [(cameras,) for cameras in [CAMERAS, {k:CAMERAS[k] for k in CAMERAS if k!='dcamera'}]] | ||||||
|  | 
 | ||||||
|  | FRAME_TOLERANCE = 0 | ||||||
|  | FILE_SIZE_TOLERANCE = 0.5 | ||||||
|  | 
 | ||||||
|  | class TestEncoder(unittest.TestCase): | ||||||
|  | 
 | ||||||
|  |   # TODO: all of loggerd should work on PC | ||||||
|  |   @classmethod | ||||||
|  |   def setUpClass(cls): | ||||||
|  |     if not (EON or TICI): | ||||||
|  |       raise unittest.SkipTest | ||||||
|  | 
 | ||||||
|  |   def setUp(self): | ||||||
|  |     self._clear_logs() | ||||||
|  | 
 | ||||||
|  |     self.segment_length = 2 | ||||||
|  |     os.environ["LOGGERD_TEST"] = "1" | ||||||
|  |     os.environ["LOGGERD_SEGMENT_LENGTH"] = str(self.segment_length) | ||||||
|  | 
 | ||||||
|  |   def tearDown(self): | ||||||
|  |     self._clear_logs() | ||||||
|  | 
 | ||||||
|  |   def _clear_logs(self): | ||||||
|  |     if os.path.exists(ROOT): | ||||||
|  |       shutil.rmtree(ROOT) | ||||||
|  | 
 | ||||||
|  |   def _get_latest_segment_path(self): | ||||||
|  |     last_route = sorted(Path(ROOT).iterdir(), key=os.path.getmtime)[-1] | ||||||
|  |     return os.path.join(ROOT, last_route) | ||||||
|  | 
 | ||||||
|  |   # TODO: this should run faster than real time | ||||||
|  |   @parameterized.expand(ALL_CAMERA_COMBINATIONS) | ||||||
|  |   @with_processes(['camerad', 'sensord', 'loggerd'], init_time=5) | ||||||
|  |   def test_log_rotation(self, cameras): | ||||||
|  |     print("checking targets:", cameras) | ||||||
|  |     Params().put("RecordFront", "1" if 'dcamera' in cameras else "0") | ||||||
|  | 
 | ||||||
|  |     num_segments = random.randint(80, 150) | ||||||
|  |     if "CI" in os.environ: | ||||||
|  |       num_segments = random.randint(15, 20) # ffprobe is slow on comma two | ||||||
|  | 
 | ||||||
|  |     # wait for loggerd to make the dir for first segment | ||||||
|  |     time.sleep(10) | ||||||
|  |     route_prefix_path = None | ||||||
|  |     with Timeout(30): | ||||||
|  |       while route_prefix_path is None: | ||||||
|  |         try: | ||||||
|  |           route_prefix_path = self._get_latest_segment_path().rsplit("--", 1)[0] | ||||||
|  |         except Exception: | ||||||
|  |           time.sleep(2) | ||||||
|  |           continue | ||||||
|  | 
 | ||||||
|  |     for i in trange(num_segments): | ||||||
|  |       # poll for next segment | ||||||
|  |       if i < num_segments - 1: | ||||||
|  |         with Timeout(self.segment_length*3, error_msg=f"timed out waiting for segment {i}"): | ||||||
|  |           while True: | ||||||
|  |             seg_num = int(self._get_latest_segment_path().rsplit("--", 1)[1]) | ||||||
|  |             if seg_num > i: | ||||||
|  |               break | ||||||
|  |             time.sleep(0.1) | ||||||
|  |       else: | ||||||
|  |         time.sleep(self.segment_length) | ||||||
|  | 
 | ||||||
|  |       # check each camera file size | ||||||
|  |       for camera, size in cameras.items(): | ||||||
|  |         ext = "ts" if camera=='qcamera' else "hevc" | ||||||
|  |         file_path = f"{route_prefix_path}--{i}/{camera}.{ext}" | ||||||
|  | 
 | ||||||
|  |         # check file size | ||||||
|  |         self.assertTrue(os.path.exists(file_path), f"couldn't find {file_path}") | ||||||
|  |         file_size = os.path.getsize(file_path) | ||||||
|  |         self.assertTrue(math.isclose(file_size, size, rel_tol=FILE_SIZE_TOLERANCE), | ||||||
|  |                         f"{camera} failed size check: expected {size}, got {file_size}") | ||||||
|  | 
 | ||||||
|  |         if camera == 'qcamera': | ||||||
|  |           continue | ||||||
|  | 
 | ||||||
|  |         # check frame count | ||||||
|  |         cmd = f"ffprobe -v error -count_frames -select_streams v:0 -show_entries stream=nb_read_frames \ | ||||||
|  |                -of default=nokey=1:noprint_wrappers=1 {file_path}" | ||||||
|  |         expected_frames = self.segment_length * CAMERA_FPS // 2 if (EON and camera=='dcamera') else self.segment_length * CAMERA_FPS | ||||||
|  |         frame_tolerance = FRAME_TOLERANCE+1 if (EON and camera=='dcamera') or i==0 else FRAME_TOLERANCE | ||||||
|  |         frame_count = int(subprocess.check_output(cmd, shell=True, encoding='utf8').strip()) | ||||||
|  | 
 | ||||||
|  |         self.assertTrue(abs(expected_frames - frame_count) <= frame_tolerance, | ||||||
|  |                         f"{camera} failed frame count check: expected {expected_frames}, got {frame_count}") | ||||||
|  |       shutil.rmtree(f"{route_prefix_path}--{i}") | ||||||
|  | 
 | ||||||
|  | if __name__ == "__main__": | ||||||
|  |   unittest.main() | ||||||
| @ -1,124 +1,211 @@ | |||||||
| #!/usr/bin/env python3 | #!/usr/bin/env python3 | ||||||
| import math |  | ||||||
| import os | import os | ||||||
| import random | import random | ||||||
| import shutil | import string | ||||||
| import subprocess | import subprocess | ||||||
| import time | import time | ||||||
| import unittest | import unittest | ||||||
| from parameterized import parameterized | from collections import defaultdict | ||||||
| from pathlib import Path | from pathlib import Path | ||||||
| from tqdm import trange |  | ||||||
| 
 | 
 | ||||||
| from common.params import Params | from cereal import log | ||||||
|  | import cereal.messaging as messaging | ||||||
|  | from cereal.services import service_list | ||||||
|  | from common.basedir import BASEDIR | ||||||
| from common.timeout import Timeout | from common.timeout import Timeout | ||||||
| from selfdrive.hardware import EON, TICI | from common.params import Params | ||||||
| from selfdrive.test.helpers import with_processes | import selfdrive.manager as manager | ||||||
| from selfdrive.loggerd.config import ROOT, CAMERA_FPS | from selfdrive.loggerd.config import ROOT | ||||||
| 
 | from selfdrive.version import version as VERSION | ||||||
| 
 | from tools.lib.logreader import LogReader | ||||||
| # baseline file sizes for a 2s segment, in bytes |  | ||||||
| FULL_SIZE = 1253786 |  | ||||||
| if EON: |  | ||||||
|   CAMERAS = { |  | ||||||
|     "fcamera": FULL_SIZE, |  | ||||||
|     "dcamera": 770920, |  | ||||||
|     "qcamera": 38533, |  | ||||||
|   } |  | ||||||
| elif TICI: |  | ||||||
|   CAMERAS = {f"{c}camera": FULL_SIZE if c!="q" else 38533 for c in ["f", "e", "d", "q"]} |  | ||||||
| else: |  | ||||||
|   CAMERAS = {} |  | ||||||
| 
 | 
 | ||||||
| ALL_CAMERA_COMBINATIONS = [(cameras,) for cameras in [CAMERAS, {k:CAMERAS[k] for k in CAMERAS if k!='dcamera'}]] | SentinelType = log.Sentinel.SentinelType | ||||||
| 
 | 
 | ||||||
| FRAME_TOLERANCE = 0 | CEREAL_SERVICES = [f for f in log.Event.schema.union_fields if f in service_list | ||||||
| FILE_SIZE_TOLERANCE = 0.5 |                    and service_list[f].should_log and "encode" not in f.lower()] | ||||||
| 
 | 
 | ||||||
| class TestLoggerd(unittest.TestCase): | class TestLoggerd(unittest.TestCase): | ||||||
| 
 | 
 | ||||||
|   # TODO: all of loggerd should work on PC |   def _get_latest_log_dir(self): | ||||||
|   @classmethod |     log_dirs = sorted(Path(ROOT).iterdir(), key=lambda f: f.stat().st_mtime) | ||||||
|   def setUpClass(cls): |     return log_dirs[-1] | ||||||
|     if not (EON or TICI): | 
 | ||||||
|       raise unittest.SkipTest |   def _get_log_dir(self, x): | ||||||
| 
 |     for p in x.split(' '): | ||||||
|   def setUp(self): |       path = Path(p.strip()) | ||||||
|     self._clear_logs() |       if path.is_dir(): | ||||||
| 
 |         return path | ||||||
|     self.segment_length = 2 |     return None | ||||||
|     os.environ["LOGGERD_TEST"] = "1" | 
 | ||||||
|     os.environ["LOGGERD_SEGMENT_LENGTH"] = str(self.segment_length) |   def _gen_bootlog(self): | ||||||
| 
 |     with Timeout(5): | ||||||
|   def tearDown(self): |       out = subprocess.check_output(["./loggerd", "--bootlog"], cwd=os.path.join(BASEDIR, "selfdrive/loggerd"), encoding='utf-8') | ||||||
|     self._clear_logs() | 
 | ||||||
| 
 |     # check existence | ||||||
|   def _clear_logs(self): |     d = self._get_log_dir(out)  | ||||||
|     if os.path.exists(ROOT): |     path = Path(os.path.join(d, "bootlog.bz2")) | ||||||
|       shutil.rmtree(ROOT) |     assert path.is_file(), "failed to create bootlog file" | ||||||
| 
 |     return path | ||||||
|   def _get_latest_segment_path(self): | 
 | ||||||
|     last_route = sorted(Path(ROOT).iterdir(), key=os.path.getmtime)[-1] |   def _check_init_data(self, msgs): | ||||||
|     return os.path.join(ROOT, last_route) |     msg = msgs[0] | ||||||
| 
 |     assert msg.which() == 'initData' | ||||||
|   # TODO: this should run faster than real time | 
 | ||||||
|   @parameterized.expand(ALL_CAMERA_COMBINATIONS) |   def _check_sentinel(self, msgs, route): | ||||||
|   @with_processes(['camerad', 'sensord', 'loggerd'], init_time=5) |     start_type = SentinelType.startOfRoute if route else SentinelType.startOfSegment | ||||||
|   def test_log_rotation(self, cameras): |     assert msgs[1].sentinel.type == start_type | ||||||
|     print("checking targets:", cameras) | 
 | ||||||
|     Params().put("RecordFront", "1" if 'dcamera' in cameras else "0") |     end_type = SentinelType.endOfRoute if route else SentinelType.endOfSegment | ||||||
| 
 |     assert msgs[-1].sentinel.type == end_type | ||||||
|     num_segments = random.randint(80, 150) | 
 | ||||||
|     if "CI" in os.environ: |   def test_init_data_values(self): | ||||||
|       num_segments = random.randint(15, 20) # ffprobe is slow on comma two |     os.environ["CLEAN"] = random.choice(["0", "1"]) | ||||||
| 
 |     os.environ["DONGLE_ID"] = ''.join(random.choice(string.printable) for n in range(random.randint(1, 100))) | ||||||
|     # wait for loggerd to make the dir for first segment | 
 | ||||||
|     time.sleep(10) |     fake_params = [ | ||||||
|     route_prefix_path = None |       ("GitCommit", "gitCommit", "commit"), | ||||||
|     with Timeout(30): |       ("GitBranch", "gitBranch", "branch"), | ||||||
|       while route_prefix_path is None: |       ("GitRemote", "gitRemote", "remote"), | ||||||
|  |     ] | ||||||
|  |     params = Params() | ||||||
|  |     for k, _, v in fake_params: | ||||||
|  |       params.put(k, v) | ||||||
|  | 
 | ||||||
|  |     lr = list(LogReader(str(self._gen_bootlog()))) | ||||||
|  |     initData = lr[0].initData | ||||||
|  | 
 | ||||||
|  |     assert initData.dirty != bool(os.environ["CLEAN"]) | ||||||
|  |     assert initData.dongleId == os.environ["DONGLE_ID"] | ||||||
|  |     assert initData.version == VERSION | ||||||
|  | 
 | ||||||
|  |     if os.path.isfile("/proc/cmdline"): | ||||||
|  |       with open("/proc/cmdline") as f: | ||||||
|  |         assert list(initData.kernelArgs) == f.read().strip().split(" ") | ||||||
|  | 
 | ||||||
|  |       with open("/proc/version") as f: | ||||||
|  |         assert initData.kernelVersion == f.read() | ||||||
|  | 
 | ||||||
|  |     for _, k, v in fake_params: | ||||||
|  |       assert getattr(initData, k) == v | ||||||
|  | 
 | ||||||
|  |   def test_bootlog(self): | ||||||
|  |     # generate bootlog with fake launch log | ||||||
|  |     launch_log = ''.join([str(random.choice(string.printable)) for _ in range(100)]) | ||||||
|  |     with open("/tmp/launch_log", "w") as f: | ||||||
|  |       f.write(launch_log) | ||||||
|  | 
 | ||||||
|  |     bootlog_path = self._gen_bootlog() | ||||||
|  |     lr = list(LogReader(str(bootlog_path))) | ||||||
|  | 
 | ||||||
|  |     # check length | ||||||
|  |     assert len(lr) == 4 # boot + initData + 2x sentinel | ||||||
|  |      | ||||||
|  |     # check initData and sentinel | ||||||
|  |     self._check_init_data(lr) | ||||||
|  |     self._check_sentinel(lr, True) | ||||||
|  | 
 | ||||||
|  |     # check msgs | ||||||
|  |     bootlog_msgs = [m for m in lr if m.which() == 'boot'] | ||||||
|  |     assert len(bootlog_msgs) == 1 | ||||||
|  | 
 | ||||||
|  |     # sanity check values | ||||||
|  |     boot = bootlog_msgs.pop().boot | ||||||
|  |     assert abs(boot.wallTimeNanos - time.time_ns()) < 5*1e9 # within 5s | ||||||
|  |     assert boot.launchLog == launch_log | ||||||
|  | 
 | ||||||
|  |     for field, path in [("lastKmsg", "console-ramoops"), ("lastPmsg", "pmsg-ramoops-0")]: | ||||||
|  |       path = Path(os.path.join("/sys/fs/pstore/", path)) | ||||||
|  |       val = b"" | ||||||
|  |       if path.is_file(): | ||||||
|  |         val = open(path, "rb").read() | ||||||
|  |       assert getattr(boot, field) == val | ||||||
|  | 
 | ||||||
|  |   def test_qlog(self): | ||||||
|  |     qlog_services = [s for s in CEREAL_SERVICES if service_list[s].decimation is not None] | ||||||
|  |     no_qlog_services = [s for s in CEREAL_SERVICES if service_list[s].decimation is None] | ||||||
|  | 
 | ||||||
|  |     services = random.sample(qlog_services, random.randint(2, 10)) + \ | ||||||
|  |                random.sample(no_qlog_services, random.randint(2, 10)) | ||||||
|  | 
 | ||||||
|  |     pm = messaging.PubMaster(services) | ||||||
|  | 
 | ||||||
|  |     # sleep enough for the first poll to time out | ||||||
|  |     # TOOD: fix loggerd bug dropping the msgs from the first poll | ||||||
|  |     manager.start_managed_process("loggerd") | ||||||
|  |     time.sleep(2) | ||||||
|  | 
 | ||||||
|  |     sent_msgs = defaultdict(list) | ||||||
|  |     for _ in range(random.randint(2, 10) * 100): | ||||||
|  |       for s in services: | ||||||
|         try: |         try: | ||||||
|           route_prefix_path = self._get_latest_segment_path().rsplit("--", 1)[0] |           m = messaging.new_message(s) | ||||||
|         except Exception: |         except Exception: | ||||||
|           time.sleep(2) |           m = messaging.new_message(s, random.randint(2, 10)) | ||||||
|           continue |         pm.send(s, m) | ||||||
| 
 |         sent_msgs[s].append(m) | ||||||
|     for i in trange(num_segments): |       time.sleep(0.01) | ||||||
|       # poll for next segment | 
 | ||||||
|       if i < num_segments - 1: |     time.sleep(1) | ||||||
|         with Timeout(self.segment_length*3, error_msg=f"timed out waiting for segment {i}"): |     manager.kill_managed_process("loggerd") | ||||||
|           while True: | 
 | ||||||
|             seg_num = int(self._get_latest_segment_path().rsplit("--", 1)[1]) |     qlog_path = os.path.join(self._get_latest_log_dir(), "qlog.bz2") | ||||||
|             if seg_num > i: |     lr = list(LogReader(qlog_path)) | ||||||
|               break | 
 | ||||||
|             time.sleep(0.1) |     # check initData and sentinel | ||||||
|  |     self._check_init_data(lr) | ||||||
|  |     self._check_sentinel(lr, True) | ||||||
|  | 
 | ||||||
|  |     recv_msgs = defaultdict(list) | ||||||
|  |     for m in lr: | ||||||
|  |       recv_msgs[m.which()].append(m) | ||||||
|  | 
 | ||||||
|  |     for s, msgs in sent_msgs.items(): | ||||||
|  |       recv_cnt = len(recv_msgs[s]) | ||||||
|  | 
 | ||||||
|  |       if s in no_qlog_services: | ||||||
|  |         # check services with no specific decimation aren't in qlog | ||||||
|  |         assert recv_cnt == 0, f"got {recv_cnt} {s} msgs in qlog" | ||||||
|       else: |       else: | ||||||
|         time.sleep(self.segment_length) |         # check logged message count matches decimation | ||||||
| 
 |         expected_cnt = len(msgs) // service_list[s].decimation | ||||||
|       # check each camera file size |         assert recv_cnt == expected_cnt, f"expected {expected_cnt} msgs for {s}, got {recv_cnt}" | ||||||
|       for camera, size in cameras.items(): | 
 | ||||||
|         ext = "ts" if camera=='qcamera' else "hevc" |   def test_rlog(self): | ||||||
|         file_path = f"{route_prefix_path}--{i}/{camera}.{ext}" |     services = random.sample(CEREAL_SERVICES, random.randint(5, 10)) | ||||||
| 
 |     pm = messaging.PubMaster(services) | ||||||
|         # check file size | 
 | ||||||
|         self.assertTrue(os.path.exists(file_path), f"couldn't find {file_path}") |     # sleep enough for the first poll to time out | ||||||
|         file_size = os.path.getsize(file_path) |     # TOOD: fix loggerd bug dropping the msgs from the first poll | ||||||
|         self.assertTrue(math.isclose(file_size, size, rel_tol=FILE_SIZE_TOLERANCE), |     manager.start_managed_process("loggerd") | ||||||
|                         f"{camera} failed size check: expected {size}, got {file_size}") |     time.sleep(2) | ||||||
| 
 | 
 | ||||||
|         if camera == 'qcamera': |     sent_msgs = defaultdict(list) | ||||||
|           continue |     for _ in range(random.randint(2, 10) * 100): | ||||||
| 
 |       for s in services: | ||||||
|         # check frame count |         try: | ||||||
|         cmd = f"ffprobe -v error -count_frames -select_streams v:0 -show_entries stream=nb_read_frames \ |           m = messaging.new_message(s) | ||||||
|                -of default=nokey=1:noprint_wrappers=1 {file_path}" |         except Exception: | ||||||
|         expected_frames = self.segment_length * CAMERA_FPS // 2 if (EON and camera=='dcamera') else self.segment_length * CAMERA_FPS |           m = messaging.new_message(s, random.randint(2, 10)) | ||||||
|         frame_tolerance = FRAME_TOLERANCE+1 if (EON and camera=='dcamera') or i==0 else FRAME_TOLERANCE |         pm.send(s, m) | ||||||
|         frame_count = int(subprocess.check_output(cmd, shell=True, encoding='utf8').strip()) |         sent_msgs[s].append(m) | ||||||
| 
 |       time.sleep(0.01) | ||||||
|         self.assertTrue(abs(expected_frames - frame_count) <= frame_tolerance, | 
 | ||||||
|                         f"{camera} failed frame count check: expected {expected_frames}, got {frame_count}") |     time.sleep(1) | ||||||
|       shutil.rmtree(f"{route_prefix_path}--{i}") |     manager.kill_managed_process("loggerd") | ||||||
|  | 
 | ||||||
|  |     lr = list(LogReader(os.path.join(self._get_latest_log_dir(), "rlog.bz2"))) | ||||||
|  | 
 | ||||||
|  |     # check initData and sentinel | ||||||
|  |     self._check_init_data(lr) | ||||||
|  |     self._check_sentinel(lr, True) | ||||||
|  | 
 | ||||||
|  |     # check all messages were logged and in order | ||||||
|  |     lr = lr[2:-1] # slice off initData and both sentinels | ||||||
|  |     for m in lr: | ||||||
|  |       sent = sent_msgs[m.which()].pop(0) | ||||||
|  |       sent.clear_write_flag() | ||||||
|  |       assert sent.to_bytes() == m.as_builder().to_bytes() | ||||||
|  | 
 | ||||||
| 
 | 
 | ||||||
| if __name__ == "__main__": | if __name__ == "__main__": | ||||||
|   unittest.main() |   unittest.main() | ||||||
|  | |||||||
					Loading…
					
					
				
		Reference in new issue