Add tests for verifying the behavior of CWakeupManager 45/264545/1
authorJi-hoon Lee <dalton.lee@samsung.com>
Fri, 24 Sep 2021 07:59:40 +0000 (16:59 +0900)
committerJi-hoon Lee <dalton.lee@samsung.com>
Fri, 24 Sep 2021 08:06:09 +0000 (17:06 +0900)
To make sure the modification made to CWakeupManager
would not cause any unexpected side-effect especially
on the observer modules, added tests for checking the
behavioral correctness of each operations.

[==========] Running 10 tests from 1 test suite.
[----------] Global test environment set-up.
[----------] 10 tests from DefaultFixture
[ RUN      ] DefaultFixture.SettingValueObserverCalledWhenLanguageChanges
[       OK ] DefaultFixture.SettingValueObserverCalledWhenLanguageChanges (2 ms)
[ RUN      ] DefaultFixture.SettingValueObserverCalledWhenAssistantAdded
[       OK ] DefaultFixture.SettingValueObserverCalledWhenAssistantAdded (2 ms)
[ RUN      ] DefaultFixture.SettingValueObserverCalledWhenAssistantDeleted
[       OK ] DefaultFixture.SettingValueObserverCalledWhenAssistantDeleted (2 ms)
[ RUN      ] DefaultFixture.WakeupEventNotTriggeredWhenInactiveMode
[       OK ] DefaultFixture.WakeupEventNotTriggeredWhenInactiveMode (1 ms)
[ RUN      ] DefaultFixture.WakeupEventTriggeredWhenVoiceKeyGetsPressed
[       OK ] DefaultFixture.WakeupEventTriggeredWhenVoiceKeyGetsPressed (1 ms)
[ RUN      ] DefaultFixture.AudioDataReceivedWhenVoiceKeyGetsPressed
[       OK ] DefaultFixture.AudioDataReceivedWhenVoiceKeyGetsPressed (12 ms)
[ RUN      ] DefaultFixture.FinishAudioDataReceivedWhenStoppedBeforeReleasingWithInterval
[       OK ] DefaultFixture.FinishAudioDataReceivedWhenStoppedBeforeReleasingWithInterval (604 ms)
[ RUN      ] DefaultFixture.FinishAudioDataReceivedWhenStoppedBeforeReleasingWithoutInterval
[       OK ] DefaultFixture.FinishAudioDataReceivedWhenStoppedBeforeReleasingWithoutInterval (303 ms)
[ RUN      ] DefaultFixture.FinishAudioDataReceivedOnResultBeforeReleasingWithInterval
[       OK ] DefaultFixture.FinishAudioDataReceivedOnResultBeforeReleasingWithInterval (607 ms)
[ RUN      ] DefaultFixture.FinishAudioDataReceivedOnResultBeforeReleasingWithoutInterval
[       OK ] DefaultFixture.FinishAudioDataReceivedOnResultBeforeReleasingWithoutInterval (303 ms)
[----------] 10 tests from DefaultFixture (1837 ms total)
[----------] Global test environment tear-down
[==========] 10 tests from 1 test suite ran. (1837 ms total)
[  PASSED  ] 10 tests.

Change-Id: I95054494410f2583874e94cd48277cc205dc1353

tests/utc/CMakeLists.txt
tests/utc/wakeup-manager/CMakeLists.txt [new file with mode: 0644]
tests/utc/wakeup-manager/test_main.cpp [new file with mode: 0644]
tests/utc/wakeup-manager/ttpo.xml [new file with mode: 0644]

index 33c0a69..4c20d2c 100644 (file)
@@ -7,3 +7,4 @@ ADD_SUBDIRECTORY(service-main)
 ADD_SUBDIRECTORY(preference-manager-vconf)
 ADD_SUBDIRECTORY(package-update-monitor)
 ADD_SUBDIRECTORY(wakeup-policy-default)
+ADD_SUBDIRECTORY(wakeup-manager)
diff --git a/tests/utc/wakeup-manager/CMakeLists.txt b/tests/utc/wakeup-manager/CMakeLists.txt
new file mode 100644 (file)
index 0000000..4075250
--- /dev/null
@@ -0,0 +1,95 @@
+# THIS FILE WAS GENERATED BY THE TIZEN TDD-PROJECT OUTLINER.
+# IT IS NOT RECOMMENDED TO MODIFY THIS FILE SINCE THE
+# MODIFICATION CAN BE OVERWRITTEN BY THE GENERATION TOOL.
+
+CMAKE_MINIMUM_REQUIRED(VERSION 2.6)
+PROJECT(ttpo-project CXX C)
+
+LINK_DIRECTORIES(${CMAKE_BINARY_DIR})
+INCLUDE_DIRECTORIES(${CMAKE_SOURCE_DIR})
+
+SET(TTPO_SRCS
+    test_main.cpp
+    ../../../plugins/wakeup-manager/src/wakeup_manager.cpp
+    ../../../plugins/wakeup-manager/src/wakeup_audio_manager.cpp
+    ../../../plugins/wakeup-manager/src/wakeup_engine_manager.cpp
+    ../../../plugins/wakeup-manager/src/wakeup_settings.cpp
+    ../../../plugins/wakeup-manager/src/dependency_resolver.cpp
+    ../../../plugins/wakeup-manager/src/assistant_config_manager.cpp
+    ../../../plugins/wakeup-manager/src/heap_tracer.cpp
+    ../../../plugins/wakeup-manager/src/wakeup_policy.cpp
+    ../../../plugins/wakeup-manager/src/wakeup_policy_default.cpp
+    ../../../plugins/wakeup-manager/src/wakeup_policy_external.cpp
+    ../../../plugins/wakeup-manager/src/wakeup_manager_wrapper.cpp
+)
+
+SET(TTPO_PKGS
+    gmock
+    dlog
+    ecore
+    vconf
+    pkgmgr-info
+    capi-media-audio-io
+    capi-appfw-application
+    capi-appfw-preference
+)
+
+SET(TTPO_INCLUDES
+    ../../../inc//
+    ../../../plugins/wakeup-manager/inc//
+)
+
+SET(TTPO_CFLAGS
+    -Wno-format-truncation
+)
+
+SET(TTPO_WRAPPERS
+    "-Wl,\
+--wrap=ecore_main_loop_thread_safe_call_async"
+)
+
+SET(TTPO_LDFLAGS
+)
+
+SET(EXTRA_CFLAGS "${EXTRA_CFLAGS} -std=c++14")
+SET(EXTRA_CFLAGS "${EXTRA_CFLAGS} -g")
+SET(EXTRA_CFLAGS "${EXTRA_CFLAGS} -O0")
+SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${EXTRA_CFLAGS}")
+SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${TTPO_CFLAGS}")
+SET(EXTRA_LDFLAGS "${TTPO_LDFLAGS}")
+
+# Find Packages
+INCLUDE(FindPkgConfig)
+pkg_check_modules(pkgs REQUIRED
+    ${TTPO_PKGS}
+)
+
+INCLUDE_DIRECTORIES(${TTPO_INCLUDES})
+
+FOREACH(flag ${pkgs_CFLAGS})
+    SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${flag}")
+ENDFOREACH(flag)
+
+FIND_PACKAGE(GTest REQUIRED)
+INCLUDE_DIRECTORIES(${GTEST_INCLUDE_DIRS})
+LINK_DIRECTORIES(${GTEST_LIBRARY_DIRS})
+
+SET(TTPO_EXECUTABLE test-wakeup-manager)
+ADD_EXECUTABLE(
+    ${TTPO_EXECUTABLE}
+    ${TTPO_SRCS}
+)
+
+TARGET_LINK_LIBRARIES(${TTPO_EXECUTABLE} ${pkgs_LDFLAGS})
+if(NOT "${EXTRA_LDFLAGS}" STREQUAL " ")
+    TARGET_LINK_LIBRARIES(${TTPO_EXECUTABLE} ${EXTRA_LDFLAGS})
+endif()
+TARGET_LINK_LIBRARIES(${TTPO_EXECUTABLE} ${GTEST_LIBRARIES} ${GTEST_MAIN_LIBRARIES})
+
+SET_TARGET_PROPERTIES(${TTPO_EXECUTABLE} PROPERTIES
+       COMPILE_FLAGS "-fPIE"
+       LINK_FLAGS "${TTPO_WRAPPERS}")
+ADD_TEST(NAME ${TTPO_EXECUTABLE} COMMAND ${TTPO_EXECUTABLE})
+
+# REPLACE THE TARGET INSTALL DESTINATION AS NEEDED (default : bin)
+INSTALL(TARGETS ${TTPO_EXECUTABLE} DESTINATION bin)
\ No newline at end of file
diff --git a/tests/utc/wakeup-manager/test_main.cpp b/tests/utc/wakeup-manager/test_main.cpp
new file mode 100644 (file)
index 0000000..46b38f2
--- /dev/null
@@ -0,0 +1,276 @@
+#include <iostream>
+#include <gtest/gtest.h>
+#include <gmock/gmock.h>
+
+#include "wakeup_manager.h"
+
+using namespace multiassistant::wakeup;
+
+extern "C" {
+/* Add wrapper functions here with __wrap_ and __real prefix
+Example :
+
+int __real_vconf_get_int(const char *in_key, int *intval);
+int __wrap_vconf_get_int(const char *in_key, int *intval)
+{
+    if (strcmp(in_key, "TestKey") == 0)
+    return 0;
+
+    return __real_vconf_get_int(in_key, intval);
+}
+*/
+void __wrap_ecore_main_loop_thread_safe_call_async(Ecore_Cb callback, void *data) {}
+
+}
+
+class CMockWakeupEventObserver : public IWakeupEventObserver
+{
+public:
+    CMockWakeupEventObserver() {
+        ON_CALL(*this, on_streaming_audio_data).WillByDefault(
+                [this](mas_speech_streaming_event_e event, void* buffer, unsigned int len) {
+                    if (event == MAS_SPEECH_STREAMING_EVENT_FINISH) ++mFinishedReceived;
+                    ++mStreamingAudioCount;
+                });
+    }
+    MOCK_METHOD(void, on_wakeup, (mas_wakeup_event_info wakeup_info), (override));
+    MOCK_METHOD(void, on_streaming_audio_data,
+        (mas_speech_streaming_event_e event, void* buffer, unsigned int len), (override));
+    MOCK_METHOD(void, on_audio_streaming_data_section, (ma_audio_streaming_data_section_e section), (override));
+    MOCK_METHOD(void, on_wakeup_engine_command,
+        (mas_wakeup_engine_command_target_e target, const char* name, const char* command), (override));
+    MOCK_METHOD(void, on_wakeup_service_state_changed, (ma_service_state_e state), (override));
+    MOCK_METHOD(void, on_voice_key_status_changed, (ma_voice_key_status_e status), (override));
+
+    std::atomic_uint mStreamingAudioCount{0};
+    std::atomic_uint mFinishedReceived{0};
+};
+
+class CMockSettingValueObserver : public ISettingValueObserver
+{
+public:
+    MOCK_METHOD(void, on_value_changed, (), (override));
+};
+
+class DefaultFixture : public testing::Test
+{
+public:
+    DefaultFixture() {
+    }
+    virtual ~DefaultFixture() {
+    }
+    void SetUp() override {
+        mWakeupManager.initialize();
+    }
+    void TearDown() override {
+        mWakeupManager.deinitialize();
+    }
+
+    /* Using NiceMock to ignore uninterested method calls to on_wakeup_service_state_changed() */
+    testing::NiceMock<CMockWakeupEventObserver> mWakeupEventObserver;
+    CMockSettingValueObserver mSettingValueObserver;
+    CWakeupManager mWakeupManager{&mWakeupEventObserver, &mSettingValueObserver};
+};
+
+TEST_F(DefaultFixture, SettingValueObserverCalledWhenLanguageChanges) {
+    EXPECT_CALL(mSettingValueObserver, on_value_changed())
+        .Times(testing::Exactly(1));
+
+    CWakeupSettings* settings = mWakeupManager.get_wakeup_settings();
+    auto observer = settings->get_observers().at(0);
+
+    observer->on_voice_input_language_changed("ko-KR");
+}
+
+TEST_F(DefaultFixture, SettingValueObserverCalledWhenAssistantAdded) {
+    EXPECT_CALL(mSettingValueObserver, on_value_changed())
+        .Times(testing::Exactly(1));
+
+    CWakeupSettings* settings = mWakeupManager.get_wakeup_settings();
+    auto observer = settings->get_observers().at(0);
+
+    observer->on_assistant_enabled_info_changed("Bixby", true);
+}
+
+TEST_F(DefaultFixture, SettingValueObserverCalledWhenAssistantDeleted) {
+    EXPECT_CALL(mSettingValueObserver, on_value_changed())
+        .Times(testing::Exactly(1));
+
+    CWakeupSettings* settings = mWakeupManager.get_wakeup_settings();
+    auto observer = settings->get_observers().at(0);
+
+    observer->on_assistant_enabled_info_changed("Bixby", false);
+}
+
+TEST_F(DefaultFixture, WakeupEventNotTriggeredWhenInactiveMode) {
+    EXPECT_CALL(mWakeupEventObserver, on_wakeup(testing::_))
+        .Times(0);
+    EXPECT_CALL(mWakeupEventObserver, on_voice_key_status_changed(testing::_))
+        .Times(0);
+
+    mWakeupManager.change_manager_state(WAKEUP_MANAGER_STATE_INACTIVE);
+    mWakeupManager.process_plugin_event(MAS_PLUGIN_EVENT_VOICE_KEY_PRESSED, nullptr, 0);
+}
+
+TEST_F(DefaultFixture, WakeupEventTriggeredWhenVoiceKeyGetsPressed) {
+    EXPECT_CALL(mWakeupEventObserver, on_wakeup(testing::_))
+        .Times(testing::Exactly(1));
+    EXPECT_CALL(mWakeupEventObserver, on_voice_key_status_changed(MA_VOICE_KEY_STATUS_PRESSED))
+        .Times(testing::Exactly(1));
+
+    mWakeupManager.change_manager_state(WAKEUP_MANAGER_STATE_LISTENING);
+    mWakeupManager.process_plugin_event(MAS_PLUGIN_EVENT_VOICE_KEY_PRESSED, nullptr, 0);
+}
+
+TEST_F(DefaultFixture, AudioDataReceivedWhenVoiceKeyGetsPressed) {
+    EXPECT_CALL(mWakeupEventObserver,
+            on_streaming_audio_data(MAS_SPEECH_STREAMING_EVENT_START, testing::_, testing::_))
+        .Times(testing::Exactly(1));
+    EXPECT_CALL(mWakeupEventObserver,
+            on_streaming_audio_data(MAS_SPEECH_STREAMING_EVENT_CONTINUE, testing::_, testing::_))
+        .Times(testing::Exactly(3));
+    EXPECT_CALL(mWakeupEventObserver,
+            on_streaming_audio_data(MAS_SPEECH_STREAMING_EVENT_FINISH, testing::_, testing::_))
+        .Times(testing::Exactly(1));
+
+    const std::chrono::seconds interval(3);
+    auto last = std::chrono::steady_clock::now();
+
+    const int len = 10;
+    unsigned char buffer[len];
+
+    mWakeupEventObserver.mStreamingAudioCount.store(0);
+
+    mWakeupManager.change_manager_state(WAKEUP_MANAGER_STATE_LISTENING);
+    mWakeupManager.process_plugin_event(MAS_PLUGIN_EVENT_VOICE_KEY_PRESSED, nullptr, 0);
+    mWakeupManager.start_streaming_utterance_data();
+    mWakeupManager.feed_audio_data(MAS_SPEECH_STREAMING_EVENT_START, buffer, len);
+    mWakeupManager.feed_audio_data(MAS_SPEECH_STREAMING_EVENT_CONTINUE, buffer, len);
+    mWakeupManager.feed_audio_data(MAS_SPEECH_STREAMING_EVENT_CONTINUE, buffer, len);
+    mWakeupManager.feed_audio_data(MAS_SPEECH_STREAMING_EVENT_CONTINUE, buffer, len);
+    mWakeupManager.feed_audio_data(MAS_SPEECH_STREAMING_EVENT_FINISH, buffer, len);
+    mWakeupManager.process_plugin_event(MAS_PLUGIN_EVENT_VOICE_KEY_RELEASED, nullptr, 0);
+
+    /* Wait for all audio data gets received, at most 3 seconds */
+    while (mWakeupEventObserver.mStreamingAudioCount.load() < 5) {
+        auto now = std::chrono::steady_clock::now();
+        if (now - last > interval) break;
+    }
+    mWakeupManager.stop_streaming_utterance_data();
+}
+
+TEST_F(DefaultFixture, FinishAudioDataReceivedWhenStoppedBeforeReleasingWithInterval) {
+    const std::chrono::seconds interval(3);
+    auto last = std::chrono::steady_clock::now();
+
+    const int len = 10;
+    unsigned char buffer[len];
+
+    mWakeupEventObserver.mFinishedReceived.store(0);
+
+    mWakeupManager.change_manager_state(WAKEUP_MANAGER_STATE_LISTENING);
+    mWakeupManager.process_plugin_event(MAS_PLUGIN_EVENT_VOICE_KEY_PRESSED, nullptr, 0);
+    mWakeupManager.start_streaming_utterance_data();
+    mWakeupManager.feed_audio_data(MAS_SPEECH_STREAMING_EVENT_START, buffer, len);
+    this_thread::sleep_for(chrono::milliseconds(300));
+    mWakeupManager.feed_audio_data(MAS_SPEECH_STREAMING_EVENT_CONTINUE, buffer, len);
+    mWakeupManager.stop_streaming_utterance_data();
+
+    /* Wait for FINISH audio data gets received, at most 3 seconds */
+    while (mWakeupEventObserver.mFinishedReceived.load() == 0) {
+        auto now = std::chrono::steady_clock::now();
+        if (now - last > interval) break;
+    }
+
+    this_thread::sleep_for(chrono::milliseconds(300));
+    ASSERT_EQ(mWakeupEventObserver.mFinishedReceived.load(), 1);
+}
+
+TEST_F(DefaultFixture, FinishAudioDataReceivedWhenStoppedBeforeReleasingWithoutInterval) {
+    const std::chrono::seconds interval(3);
+    auto last = std::chrono::steady_clock::now();
+
+    const int len = 10;
+    unsigned char buffer[len];
+
+    mWakeupEventObserver.mFinishedReceived.store(0);
+
+    mWakeupManager.change_manager_state(WAKEUP_MANAGER_STATE_LISTENING);
+    mWakeupManager.process_plugin_event(MAS_PLUGIN_EVENT_VOICE_KEY_PRESSED, nullptr, 0);
+    mWakeupManager.feed_audio_data(MAS_SPEECH_STREAMING_EVENT_START, buffer, len);
+    mWakeupManager.feed_audio_data(MAS_SPEECH_STREAMING_EVENT_CONTINUE, buffer, len);
+    mWakeupManager.start_streaming_utterance_data();
+    mWakeupManager.stop_streaming_utterance_data();
+
+    /* Wait for FINISH audio data gets received, at most 3 seconds */
+    while (mWakeupEventObserver.mFinishedReceived.load() == 0) {
+        auto now = std::chrono::steady_clock::now();
+        if (now - last > interval) break;
+    }
+
+    this_thread::sleep_for(chrono::milliseconds(300));
+    ASSERT_EQ(mWakeupEventObserver.mFinishedReceived.load(), 1);
+}
+
+TEST_F(DefaultFixture, FinishAudioDataReceivedOnResultBeforeReleasingWithInterval) {
+    const std::chrono::seconds interval(3);
+    auto last = std::chrono::steady_clock::now();
+
+    const int len = 10;
+    unsigned char buffer[len];
+
+    mWakeupEventObserver.mFinishedReceived.store(0);
+
+    mWakeupManager.change_manager_state(WAKEUP_MANAGER_STATE_LISTENING);
+    mWakeupManager.process_plugin_event(MAS_PLUGIN_EVENT_VOICE_KEY_PRESSED, nullptr, 0);
+    mWakeupManager.start_streaming_utterance_data();
+    mWakeupManager.feed_audio_data(MAS_SPEECH_STREAMING_EVENT_START, buffer, len);
+    this_thread::sleep_for(chrono::milliseconds(300));
+    mWakeupManager.feed_audio_data(MAS_SPEECH_STREAMING_EVENT_CONTINUE, buffer, len);
+    mWakeupManager.update_recognition_result(std::string(), 0);
+
+    /* Wait for FINISH audio data gets received, at most 3 seconds */
+    while (mWakeupEventObserver.mFinishedReceived.load() == 0) {
+        auto now = std::chrono::steady_clock::now();
+        if (now - last > interval) break;
+    }
+
+    this_thread::sleep_for(chrono::milliseconds(300));
+    ASSERT_EQ(mWakeupEventObserver.mFinishedReceived.load(), 1);
+}
+
+TEST_F(DefaultFixture, FinishAudioDataReceivedOnResultBeforeReleasingWithoutInterval) {
+    const std::chrono::seconds interval(3);
+    auto last = std::chrono::steady_clock::now();
+
+    const int len = 10;
+    unsigned char buffer[len];
+
+    mWakeupEventObserver.mFinishedReceived.store(0);
+
+    mWakeupManager.change_manager_state(WAKEUP_MANAGER_STATE_LISTENING);
+    mWakeupManager.process_plugin_event(MAS_PLUGIN_EVENT_VOICE_KEY_PRESSED, nullptr, 0);
+    mWakeupManager.feed_audio_data(MAS_SPEECH_STREAMING_EVENT_START, buffer, len);
+    mWakeupManager.feed_audio_data(MAS_SPEECH_STREAMING_EVENT_CONTINUE, buffer, len);
+    mWakeupManager.start_streaming_utterance_data();
+    mWakeupManager.update_recognition_result(std::string(), 0);
+
+    /* Wait for FINISH audio data gets received, at most 3 seconds */
+    while (mWakeupEventObserver.mFinishedReceived.load() == 0) {
+        auto now = std::chrono::steady_clock::now();
+        if (now - last > interval) break;
+    }
+
+    this_thread::sleep_for(chrono::milliseconds(300));
+    ASSERT_EQ(mWakeupEventObserver.mFinishedReceived.load(), 1);
+}
+
+/* Uncomment the below piece of code if you need your own main() */
+/*
+int main(int argc, char** argv) {
+    std::cout << "Starting tests" << std::endl;
+    testing::InitGoogleTest(&argc, argv);
+    int ret = RUN_ALL_TESTS();
+    return ret;
+}
+*/
diff --git a/tests/utc/wakeup-manager/ttpo.xml b/tests/utc/wakeup-manager/ttpo.xml
new file mode 100644 (file)
index 0000000..85209a9
--- /dev/null
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ttpo>
+    <source_files>test_main.cpp</source_files>
+    <source_files>../../../plugins/wakeup-manager/src/wakeup_manager.cpp</source_files>
+    <source_files>../../../plugins/wakeup-manager/src/wakeup_audio_manager.cpp</source_files>
+    <source_files>../../../plugins/wakeup-manager/src/wakeup_engine_manager.cpp</source_files>
+    <source_files>../../../plugins/wakeup-manager/src/wakeup_settings.cpp</source_files>
+    <source_files>../../../plugins/wakeup-manager/src/dependency_resolver.cpp</source_files>
+    <source_files>../../../plugins/wakeup-manager/src/assistant_config_manager.cpp</source_files>
+    <source_files>../../../plugins/wakeup-manager/src/heap_tracer.cpp</source_files>
+    <source_files>../../../plugins/wakeup-manager/src/wakeup_policy.cpp</source_files>
+    <source_files>../../../plugins/wakeup-manager/src/wakeup_policy_default.cpp</source_files>
+    <source_files>../../../plugins/wakeup-manager/src/wakeup_policy_external.cpp</source_files>
+    <source_files>../../../plugins/wakeup-manager/src/wakeup_manager_wrapper.cpp</source_files>
+    <include_directories>../../../inc/</include_directories>
+    <include_directories>../../../plugins/wakeup-manager/inc/</include_directories>
+    <required_packages>gmock</required_packages>
+    <required_packages>dlog</required_packages>
+    <required_packages>ecore</required_packages>
+    <required_packages>vconf</required_packages>
+    <required_packages>pkgmgr-info</required_packages>
+    <required_packages>capi-media-audio-io</required_packages>
+    <required_packages>capi-appfw-application</required_packages>
+    <required_packages>capi-appfw-preference</required_packages>
+    <wrapper_functions>ecore_main_loop_thread_safe_call_async</wrapper_functions>
+    <cflags>-Wno-format-truncation</cflags>
+</ttpo>