#!/usr/bin/env python3 # # Copyright (C) 2024 The Android Open Source Project # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # import unittest from report_fuchsia import Stacker from etm_types import InstrSubtype from . test_utils import TestBase class Result: def __init__(self): self.events = [] def duration(self, begin, thread, category, name, timestamp): self.events.append(('call' if begin else 'ret', name, timestamp)) class TestEtmStacker(TestBase): def setUp(self): super().setUp() self.r = Result() self.s = Stacker(self.r, 0) def assertResult(self, sequence): self.s.flush() self.assertEqual(self.r.events, sequence) # Primitives. def test_primitives_call(self): self.s.call('main', 1) self.assertResult([('call', 'main', 1)]) def test_primitives_ret(self): self.s.call('main', 1) self.s.ret('main', 2) self.assertResult([('call', 'main', 1), ('ret', 'main', 2)]) def test_primitives_no_timestamp(self): self.s.call('main', None) self.s.ret('main', None) self.assertResult([]) # Simple stacks. def test_simple_start(self): self.s.instr_range(1, 'main', 0, 'main', 0, InstrSubtype.V8_RET) self.assertResult([('call', 'main', 1), ('ret', 'main', 1)]) def test_simple_return(self): self.s.instr_range(1, 'main', 0, 'main', 0, InstrSubtype.V8_RET) self.assertResult([('call', 'main', 1), ('ret', 'main', 1)]) def test_simple_inner(self): self.s.instr_range(1, 'main', 0, 'main', 0, InstrSubtype.BR_LINK) self.s.instr_range(2, 'inner', 0, 'inner', 0, InstrSubtype.V8_RET) self.assertResult([('call', 'main', 1), ('call', 'inner', 2), ('ret', 'inner', 2)]) def test_simple_recursion(self): self.s.instr_range(1, 'main', 0, 'main', 0, InstrSubtype.BR_LINK) self.s.instr_range(2, 'main', 0, 'main', 0, InstrSubtype.V8_RET) self.s.instr_range(3, 'main', 0, 'main', 0, InstrSubtype.V8_RET) self.assertResult([('call', 'main', 1), ('call', 'main', 2), ('ret', 'main', 2), ('ret', 'main', 3)]) def test_simple_tailcall(self): # If not a return or a call, the jump is considered a tail call. self.s.instr_range(1, 'main', 0, 'main', 0, None) self.s.instr_range(2, 'next', 0, 'next', 0, None) self.assertResult([('call', 'main', 1), ('ret', 'main', 2), ('call', 'next', 2)]) def test_simple_tailcall_single(self): # If the symbol is different between the start and the end, it changed without a jump. # Pretend that it is a tail-call. self.s.instr_range(1, 'main', 0, 'next', 0, None) self.assertResult([('call', 'main', 1), ('ret', 'main', 1), ('call', 'next', 1)]) def test_simple_no_call(self): # If execution returns below the known stack, pretend that we have known about the function # running since the first known timestamp. self.s.instr_range(1, 'main', 0, 'main', 0, None) self.s.instr_range(2, 'main', 0, 'main', 0, InstrSubtype.V8_RET) self.s.instr_range(3, 'next', 0, 'next', 0, None) self.assertResult([('call', 'next', 1), ('call', 'main', 1), ('ret', 'main', 2)]) # Delayed events (i.e., no timestamps). def test_delay_later(self): self.s.instr_range(None, 'main', 0, 'main', 0, None) self.s.timestamp(1) self.assertResult([('call', 'main', 1)]) def test_delay_later_ret(self): self.s.instr_range(None, 'main', 0, 'main', 0, InstrSubtype.V8_RET) self.s.timestamp(1) self.assertResult([('call', 'main', 1), ('ret', 'main', 1)]) def test_delay_inner(self): self.s.instr_range(None, 'main', 0, 'main', 0, InstrSubtype.BR_LINK) self.s.instr_range(2, 'inner', 0, 'inner', 0, InstrSubtype.V8_RET) self.assertResult([('call', 'main', 2), ('call', 'inner', 2), ('ret', 'inner', 2)]) def test_delay_no_call(self): self.s.instr_range(None, 'main', 0, 'main', 0, InstrSubtype.V8_RET) self.s.instr_range(2, 'next', 0, 'next', 0, None) self.assertResult([('call', 'next', 2), ('call', 'main', 2), ('ret', 'main', 2)]) # Gaps. def test_gaps_gap(self): self.s.instr_range(1, 'main', 0, 'main', 0, None) self.s.gap(2) self.s.instr_range(3, 'next', 0, 'next', 0, None) self.assertResult([('call', 'main', 1), ('ret', 'main', 2), ('call', 'next', 3)]) def test_gaps_exception_start(self): self.s.exception(1, 'exception', 0) self.assertResult([('call', 'exception', 1)]) def test_gaps_exception_twice(self): self.s.exception(1, 'exception', 0) self.s.gap(2) self.s.exception(2, 'another', 0) self.assertResult([('call', 'exception', 1), ('ret', 'exception', 2), ('call', 'another', 2)]) def test_gaps_exception_ret(self): self.s.instr_range(1, 'main', 0, 'main', 0, None) self.s.exception(2, 'exception', 1) self.s.gap(2) self.s.instr_range(3, 'main', 1, 'main', 1, InstrSubtype.V8_RET) self.assertResult([('call', 'main', 1), ('call', 'exception', 2), ('ret', 'exception', 3), ('ret', 'main', 3)]) def test_gaps_exception_ret_twice(self): self.s.instr_range(1, 'main', 0, 'main', 0, None) self.s.exception(2, 'exception', 1) self.s.gap(2) self.s.exception(3, 'another', 1) self.s.gap(3) self.s.instr_range(4, 'main', 1, 'main', 1, InstrSubtype.V8_RET) self.assertResult([('call', 'main', 1), ('call', 'exception', 2), ('ret', 'exception', 3), ('call', 'another', 3), ('ret', 'another', 4), ('ret', 'main', 4)]) def test_gaps_exception_ret_bad(self): self.s.instr_range(1, 'main', 0, 'main', 0, None) self.s.exception(2, 'exception', 100) self.s.gap(2) self.s.instr_range(3, 'next', 1, 'next', 1, None) self.assertResult([('call', 'main', 1), ('call', 'exception', 2), ('ret', 'exception', 3), ('ret', 'main', 3), ('call', 'next', 3)]) def test_gaps_exception_ret_twice_bad(self): self.s.instr_range(1, 'main', 0, 'main', 0, None) self.s.exception(2, 'exception', 1) self.s.gap(2) self.s.exception(3, 'another', 2) self.s.gap(3) self.s.instr_range(4, 'next', 3, 'next', 3, None) self.assertResult([('call', 'main', 1), ('call', 'exception', 2), ('ret', 'exception', 3), ('ret', 'main', 3), ('call', 'another', 3), ('ret', 'another', 4), ('call', 'next', 4)]) def test_gaps_exception_call_preserved(self): self.s.instr_range(1, 'main', 0, 'main', 0, InstrSubtype.BR_LINK) self.s.exception(2, 'exception', 1) self.s.gap(2) self.s.instr_range(3, 'next', 1, 'next', 2, None) self.assertResult([('call', 'main', 1), ('call', 'exception', 2), ('ret', 'exception', 3), ('call', 'next', 3)]) def test_gaps_exception_ret_preserved(self): # The actual ret is not preserved, but the starting point of elements below the known stack # should be. self.s.instr_range(1, 'main', 0, 'main', 0, InstrSubtype.V8_RET) self.s.exception(2, 'exception', 1) self.s.gap(2) self.s.instr_range(3, 'next', 1, 'next', 2, None) self.assertResult([('call', 'next', 1), ('call', 'main', 1), ('ret', 'main', 1), ('call', 'exception', 2), ('ret', 'exception', 3)]) def test_gaps_plt(self): self.s.instr_range(1, 'main', 0, 'main', 0, InstrSubtype.BR_LINK) self.s.instr_range(2, 'func@plt', 0, 'func@plt', 0, None) self.s.gap(3) self.s.instr_range(4, 'main', 0, 'main', 0, None) self.s.instr_range(5, 'main', 0, 'main', 0, InstrSubtype.V8_RET) self.assertResult([('call', 'main', 1), ('call', 'func@plt', 2), ('ret', 'func@plt', 4), ('ret', 'main', 5)]) def test_gaps_plt_wrong(self): self.s.instr_range(1, 'main', 0, 'main', 0, InstrSubtype.BR_LINK) self.s.instr_range(2, 'func@plt', 0, 'func@plt', 0, None) self.s.gap(3) self.s.instr_range(4, 'next', 0, 'next', 0, None) self.assertResult([('call', 'main', 1), ('call', 'func@plt', 2), ('ret', 'func@plt', 4), ('ret', 'main', 4), ('call', 'next', 4)]) def test_gaps_danger(self): self.s.instr_range(1, 'main', 0, 'main', 0, None) self.s.exception(2, 'exception', 100) self.s.gap(2) self.s.instr_range(3, 'main', 0, 'main', 0, InstrSubtype.V8_RET) self.assertResult([('call', 'main', 1), ('call', 'exception', 2), ('ret', 'exception', 3), ('ret', 'main', 3)]) # Stack was lost. def test_lost_lost(self): self.s.instr_range(1, 'main', 0, 'main', 0, InstrSubtype.BR_LINK) self.s.instr_range(2, 'next', 0, 'next', 0, None) self.s.lost_stack(3) self.assertResult([('call', 'main', 1), ('call', 'next', 2), ('ret', 'next', 3), ('ret', 'main', 3)]) def test_lost_ordering(self): self.s.exception(1, 'exception', 0) self.s.gap(None) self.s.lost_stack(None) self.s.timestamp(2) self.assertResult([('call', 'exception', 1), ('ret', 'exception', 2)]) def test_lost_assumption(self): self.s.instr_range(1, 'main', 0, 'main', 0, None) self.s.exception(2, 'exception', 0) self.s.gap(2) self.s.instr_range(3, 'main', 0, 'main', 0, None) self.assertResult([('call', 'main', 1), ('call', 'exception', 2), ('ret', 'exception', 3)]) def test_lost_late(self): self.s.instr_range(None, 'main', 0, 'main', 0, InstrSubtype.BR_LINK) self.s.exception(None, 'exception', 0) self.s.gap(None) self.s.instr_range(1, 'next', 0, 'next', 0, None) self.assertResult([('call', 'main', 1), ('call', 'exception', 1), ('ret', 'exception', 1), ('call', 'next', 1)]) # Timestamps. def test_timestamps_updates(self): self.s.timestamp(10) self.assertEqual(self.s.last_timestamp, 10) def test_timestamps_gap_removes(self): self.s.timestamp(10) self.s.gap(11) self.assertEqual(self.s.last_timestamp, None) def test_timestamps_gap_removes_exception(self): self.s.timestamp(10) self.s.exception(10, "exception", 0) self.s.gap(10) self.assertEqual(self.s.last_timestamp, None) def test_timestamps_gap_removes_plt(self): self.s.instr_range(1, 'foo@plt', 0, 'foo@plt', 0, None) self.s.gap(10) self.assertEqual(self.s.last_timestamp, None) def test_timestamps_gap_again(self): self.s.timestamp(10) self.s.gap(11) self.s.timestamp(11) self.assertEqual(self.s.last_timestamp, 11) def test_timestamps_first(self): self.s.instr_range(1, 'main', 0, 'main', 0, None) self.s.instr_range(2, 'main', 0, 'main', 0, None) self.assertEqual(self.s.first_timestamp, 1) self.assertEqual(self.s.last_timestamp, 2) def test_timestamps_first_gap(self): self.s.instr_range(1, 'main', 0, 'main', 0, None) self.s.gap(10) self.s.instr_range(11, 'main', 0, 'main', 0, None) self.assertEqual(self.s.first_timestamp, 11) def test_timestamps_first_exception_good(self): self.s.instr_range(1, 'main', 0, 'main', 0, None) self.s.exception(2, 'exception', 100) self.s.gap(10) self.s.instr_range(11, 'main', 100, 'main', 100, None) self.assertEqual(self.s.first_timestamp, 1) def test_timestamps_first_exception_bad(self): self.s.instr_range(1, 'main', 0, 'main', 0, None) self.s.exception(2, 'exception', 100) self.s.gap(10) self.s.instr_range(11, 'next', 200, 'next', 200, None) self.assertEqual(self.s.first_timestamp, 11) def test_timestamps_first_plt_good(self): self.s.instr_range(1, 'main', 0, 'main', 0, InstrSubtype.BR_LINK) self.s.instr_range(2, 'foo@plt', 0, 'foo@plt', 0, None) self.s.gap(10) self.s.instr_range(11, 'main', 200, 'main', 200, None) self.assertEqual(self.s.first_timestamp, 1) def test_timestamps_first_plt_bad(self): self.s.instr_range(1, 'main', 0, 'main', 0, InstrSubtype.BR_LINK) self.s.instr_range(2, 'foo@plt', 0, 'foo@plt', 0, None) self.s.gap(10) self.s.instr_range(11, 'next', 200, 'next', 200, None) self.assertEqual(self.s.first_timestamp, 11) def test_timestamps_exception_fake_start(self): self.s.exception(1, 'exception', 100) self.s.lost_stack(None) self.s.instr_range(None, 'main', 0, 'main', 0, InstrSubtype.V8_RET) self.s.instr_range(2, 'deep', 0, 'deep', 0, None) self.assertResult([('call', 'exception', 1), ('ret', 'exception', 2), ('call', 'deep', 2), ('call', 'main', 2), ('ret', 'main', 2)])