import shutil
import tempfile
import os
import unittest
import pytest
import requests
from parameterized import parameterized
from unittest import mock
from openpilot . tools . lib . logreader import LogIterable , LogReader , comma_api_source , parse_indirect , parse_slice , ReadMode
from openpilot . tools . lib . route import SegmentRange
NUM_SEGS = 17 # number of segments in the test route
ALL_SEGS = list ( range ( NUM_SEGS ) )
TEST_ROUTE = " 344c5c15b34f2d8a/2024-01-03--09-37-12 "
QLOG_FILE = " https://commadataci.blob.core.windows.net/openpilotci/0375fdf7b1ce594d/2019-06-13--08-32-25/3/qlog.bz2 "
def noop ( segment : LogIterable ) :
return segment
class TestLogReader ( unittest . TestCase ) :
@parameterized . expand ( [
( f " { TEST_ROUTE } " , ALL_SEGS ) ,
( f " { TEST_ROUTE . replace ( ' / ' , ' | ' ) } " , ALL_SEGS ) ,
( f " { TEST_ROUTE } --0 " , [ 0 ] ) ,
( f " { TEST_ROUTE } --5 " , [ 5 ] ) ,
( f " { TEST_ROUTE } /0 " , [ 0 ] ) ,
( f " { TEST_ROUTE } /5 " , [ 5 ] ) ,
( f " { TEST_ROUTE } /0:10 " , ALL_SEGS [ 0 : 10 ] ) ,
( f " { TEST_ROUTE } /0:0 " , [ ] ) ,
( f " { TEST_ROUTE } /4:6 " , ALL_SEGS [ 4 : 6 ] ) ,
( f " { TEST_ROUTE } /0:-1 " , ALL_SEGS [ 0 : - 1 ] ) ,
( f " { TEST_ROUTE } /:5 " , ALL_SEGS [ : 5 ] ) ,
( f " { TEST_ROUTE } /2: " , ALL_SEGS [ 2 : ] ) ,
( f " { TEST_ROUTE } /2:-1 " , ALL_SEGS [ 2 : - 1 ] ) ,
( f " { TEST_ROUTE } /-1 " , [ ALL_SEGS [ - 1 ] ] ) ,
( f " { TEST_ROUTE } /-2 " , [ ALL_SEGS [ - 2 ] ] ) ,
( f " { TEST_ROUTE } /-2:-1 " , ALL_SEGS [ - 2 : - 1 ] ) ,
( f " { TEST_ROUTE } /-4:-2 " , ALL_SEGS [ - 4 : - 2 ] ) ,
( f " { TEST_ROUTE } /:10:2 " , ALL_SEGS [ : 10 : 2 ] ) ,
( f " { TEST_ROUTE } /5::2 " , ALL_SEGS [ 5 : : 2 ] ) ,
( f " https://useradmin.comma.ai/?onebox= { TEST_ROUTE } " , ALL_SEGS ) ,
( f " https://useradmin.comma.ai/?onebox= { TEST_ROUTE . replace ( ' / ' , ' | ' ) } " , ALL_SEGS ) ,
( f " https://useradmin.comma.ai/?onebox= { TEST_ROUTE . replace ( ' / ' , ' % 7C ' ) } " , ALL_SEGS ) ,
( f " https://cabana.comma.ai/?route= { TEST_ROUTE } " , ALL_SEGS ) ,
] )
def test_indirect_parsing ( self , identifier , expected ) :
parsed , _ , _ = parse_indirect ( identifier )
sr = SegmentRange ( parsed )
segs = parse_slice ( sr )
self . assertListEqual ( list ( segs ) , expected )
@parameterized . expand ( [
( f " { TEST_ROUTE } " , f " { TEST_ROUTE } " ) ,
( f " { TEST_ROUTE . replace ( ' / ' , ' | ' ) } " , f " { TEST_ROUTE } " ) ,
( f " { TEST_ROUTE } --5 " , f " { TEST_ROUTE } /5 " ) ,
( f " { TEST_ROUTE } /0/q " , f " { TEST_ROUTE } /0/q " ) ,
( f " { TEST_ROUTE } /5:6/r " , f " { TEST_ROUTE } /5:6/r " ) ,
( f " { TEST_ROUTE } /5 " , f " { TEST_ROUTE } /5 " ) ,
] )
def test_canonical_name ( self , identifier , expected ) :
sr = SegmentRange ( identifier )
self . assertEqual ( str ( sr ) , expected )
def test_direct_parsing ( self ) :
qlog = tempfile . NamedTemporaryFile ( mode = ' wb ' , delete = False )
with requests . get ( QLOG_FILE , stream = True ) as r :
with qlog as f :
shutil . copyfileobj ( r . raw , f )
for f in [ QLOG_FILE , qlog . name ] :
l = len ( list ( LogReader ( f ) ) )
self . assertGreater ( l , 100 )
@parameterized . expand ( [
( f " { TEST_ROUTE } /// " , ) ,
( f " { TEST_ROUTE } --- " , ) ,
( f " { TEST_ROUTE } /-4:--2 " , ) ,
( f " { TEST_ROUTE } /-a " , ) ,
( f " { TEST_ROUTE } /j " , ) ,
( f " { TEST_ROUTE } /0:1:2:3 " , ) ,
( f " { TEST_ROUTE } /:::3 " , ) ,
] )
def test_bad_ranges ( self , segment_range ) :
with self . assertRaises ( AssertionError ) :
sr = SegmentRange ( segment_range )
parse_slice ( sr )
@parameterized . expand ( [
( f " { TEST_ROUTE } /0 " , False ) ,
( f " { TEST_ROUTE } /:2 " , False ) ,
( f " { TEST_ROUTE } /0: " , True ) ,
( f " { TEST_ROUTE } /-1 " , True ) ,
( f " { TEST_ROUTE } " , True ) ,
] )
def test_slicing_api_call ( self , segment_range , api_call ) :
with mock . patch ( " openpilot.tools.lib.route.get_max_seg_number_cached " ) as max_seg_mock :
max_seg_mock . return_value = NUM_SEGS
parse_slice ( SegmentRange ( segment_range ) )
self . assertEqual ( api_call , max_seg_mock . called )
@pytest . mark . slow
def test_modes ( self ) :
qlog_len = len ( list ( LogReader ( f " { TEST_ROUTE } /0 " , ReadMode . QLOG ) ) )
rlog_len = len ( list ( LogReader ( f " { TEST_ROUTE } /0 " , ReadMode . RLOG ) ) )
self . assertLess ( qlog_len * 6 , rlog_len )
@pytest . mark . slow
def test_modes_from_name ( self ) :
qlog_len = len ( list ( LogReader ( f " { TEST_ROUTE } /0/q " ) ) )
rlog_len = len ( list ( LogReader ( f " { TEST_ROUTE } /0/r " ) ) )
self . assertLess ( qlog_len * 6 , rlog_len )
@pytest . mark . slow
def test_list ( self ) :
qlog_len = len ( list ( LogReader ( f " { TEST_ROUTE } /0/q " ) ) )
qlog_len_2 = len ( list ( LogReader ( [ f " { TEST_ROUTE } /0/q " , f " { TEST_ROUTE } /0/q " ] ) ) )
self . assertEqual ( qlog_len * 2 , qlog_len_2 )
@pytest . mark . slow
@mock . patch ( " openpilot.tools.lib.logreader._LogFileReader " )
def test_multiple_iterations ( self , init_mock ) :
lr = LogReader ( f " { TEST_ROUTE } /0/q " )
qlog_len1 = len ( list ( lr ) )
qlog_len2 = len ( list ( lr ) )
# ensure we don't create multiple instances of _LogFileReader, which means downloading the files twice
self . assertEqual ( init_mock . call_count , 1 )
self . assertEqual ( qlog_len1 , qlog_len2 )
@pytest . mark . slow
def test_helpers ( self ) :
lr = LogReader ( f " { TEST_ROUTE } /0/q " )
self . assertEqual ( lr . first ( " carParams " ) . carFingerprint , " SUBARU OUTBACK 6TH GEN " )
self . assertTrue ( 0 < len ( list ( lr . filter ( " carParams " ) ) ) < len ( list ( lr ) ) )
@parameterized . expand ( [ ( True , ) , ( False , ) ] )
@pytest . mark . slow
def test_run_across_segments ( self , cache_enabled ) :
os . environ [ " FILEREADER_CACHE " ] = " 1 " if cache_enabled else " 0 "
lr = LogReader ( f " { TEST_ROUTE } /0:4 " )
self . assertEqual ( len ( lr . run_across_segments ( 4 , noop ) ) , len ( list ( lr ) ) )
@pytest . mark . slow
def test_auto_mode ( self ) :
lr = LogReader ( f " { TEST_ROUTE } /0/q " )
qlog_len = len ( list ( lr ) )
with mock . patch ( " openpilot.tools.lib.route.Route.log_paths " ) as log_paths_mock :
log_paths_mock . return_value = [ None ] * NUM_SEGS
# Should fall back to qlogs since rlogs are not available
lr = LogReader ( f " { TEST_ROUTE } /0/a " , default_source = comma_api_source )
log_len = len ( list ( lr ) )
self . assertEqual ( qlog_len , log_len )
if __name__ == " __main__ " :
unittest . main ( )