#
# 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
import builtins
from unittest import mock
from config_builder import build_default_config, build_custom_config
from command import ProfilerCommand
from torq import DEFAULT_DUR_MS

TEST_FAILURE_MSG = "Test failure."
TEST_DUR_MS = 9000
INVALID_DUR_MS = "invalid-dur-ms"
ANDROID_SDK_VERSION_T = 33
ANDROID_SDK_VERSION_S_V2 = 32

COMMON_DEFAULT_CONFIG_BEGINNING_STRING_1 = f'''\
<<EOF

buffers: {{
  size_kb: 4096
  fill_policy: RING_BUFFER
}}
buffers {{
  size_kb: 4096
  fill_policy: RING_BUFFER
}}
buffers: {{
  size_kb: 260096
  fill_policy: RING_BUFFER
}}

data_sources: {{
  config {{
    name: "linux.process_stats"
    process_stats_config {{
      scan_all_processes_on_start: true
    }}
  }}
}}

data_sources: {{
  config {{
    name: "android.log"
    android_log_config {{
    }}
  }}
}}

data_sources {{
  config {{
    name: "android.packages_list"
  }}
}}

data_sources: {{
  config {{
    name: "linux.sys_stats"
    target_buffer: 1
    sys_stats_config {{
      stat_period_ms: 500
      stat_counters: STAT_CPU_TIMES
      stat_counters: STAT_FORK_COUNT
      meminfo_period_ms: 1000
      meminfo_counters: MEMINFO_ACTIVE_ANON
      meminfo_counters: MEMINFO_ACTIVE_FILE
      meminfo_counters: MEMINFO_INACTIVE_ANON
      meminfo_counters: MEMINFO_INACTIVE_FILE
      meminfo_counters: MEMINFO_KERNEL_STACK
      meminfo_counters: MEMINFO_MLOCKED
      meminfo_counters: MEMINFO_SHMEM
      meminfo_counters: MEMINFO_SLAB
      meminfo_counters: MEMINFO_SLAB_UNRECLAIMABLE
      meminfo_counters: MEMINFO_VMALLOC_USED
      meminfo_counters: MEMINFO_MEM_FREE
      meminfo_counters: MEMINFO_SWAP_FREE
      vmstat_period_ms: 1000
      vmstat_counters: VMSTAT_PGFAULT
      vmstat_counters: VMSTAT_PGMAJFAULT
      vmstat_counters: VMSTAT_PGFREE
      vmstat_counters: VMSTAT_PGPGIN
      vmstat_counters: VMSTAT_PGPGOUT
      vmstat_counters: VMSTAT_PSWPIN
      vmstat_counters: VMSTAT_PSWPOUT
      vmstat_counters: VMSTAT_PGSCAN_DIRECT
      vmstat_counters: VMSTAT_PGSTEAL_DIRECT
      vmstat_counters: VMSTAT_PGSCAN_KSWAPD
      vmstat_counters: VMSTAT_PGSTEAL_KSWAPD
      vmstat_counters: VMSTAT_WORKINGSET_REFAULT'''

CPUFREQ_STRING_NEW_ANDROID = f'      cpufreq_period_ms: 500'

COMMON_DEFAULT_CONFIG_BEGINNING_STRING_2 = f'''\
    }}
  }}
}}

data_sources: {{
  config {{
    name: "android.surfaceflinger.frametimeline"
    target_buffer: 2
  }}
}}

data_sources: {{
  config {{
    name: "linux.ftrace"
    target_buffer: 2
    ftrace_config {{'''

COMMON_DEFAULT_CONFIG_MIDDLE_STRING = f'''\
      atrace_categories: "aidl"
      atrace_categories: "am"
      atrace_categories: "dalvik"
      atrace_categories: "binder_lock"
      atrace_categories: "binder_driver"
      atrace_categories: "bionic"
      atrace_categories: "camera"
      atrace_categories: "disk"
      atrace_categories: "freq"
      atrace_categories: "idle"
      atrace_categories: "gfx"
      atrace_categories: "hal"
      atrace_categories: "input"
      atrace_categories: "pm"
      atrace_categories: "power"
      atrace_categories: "res"
      atrace_categories: "rro"
      atrace_categories: "sched"
      atrace_categories: "sm"
      atrace_categories: "ss"
      atrace_categories: "thermal"
      atrace_categories: "video"
      atrace_categories: "view"
      atrace_categories: "wm"
      atrace_apps: "lmkd"
      atrace_apps: "system_server"
      atrace_apps: "com.android.systemui"
      atrace_apps: "com.google.android.gms"
      atrace_apps: "com.google.android.gms.persistent"
      atrace_apps: "android:ui"
      atrace_apps: "com.google.android.apps.maps"
      atrace_apps: "*"
      buffer_size_kb: 16384
      drain_period_ms: 150
      symbolize_ksyms: true
    }}
  }}
}}'''

COMMON_CONFIG_ENDING_STRING = f'''\
write_into_file: true
file_write_period_ms: 5000
max_file_size_bytes: 100000000000
flush_period_ms: 5000
incremental_state_config {{
  clear_period_ms: 5000
}}

'''

COMMON_DEFAULT_FTRACE_EVENTS = f'''\
      ftrace_events: "dmabuf_heap/dma_heap_stat"
      ftrace_events: "ftrace/print"
      ftrace_events: "gpu_mem/gpu_mem_total"
      ftrace_events: "ion/ion_stat"
      ftrace_events: "kmem/ion_heap_grow"
      ftrace_events: "kmem/ion_heap_shrink"
      ftrace_events: "kmem/rss_stat"
      ftrace_events: "lowmemorykiller/lowmemory_kill"
      ftrace_events: "mm_event/mm_event_record"
      ftrace_events: "oom/mark_victim"
      ftrace_events: "oom/oom_score_adj_update"
      ftrace_events: "power/cpu_frequency"
      ftrace_events: "power/cpu_idle"
      ftrace_events: "power/gpu_frequency"
      ftrace_events: "power/suspend_resume"
      ftrace_events: "power/wakeup_source_activate"
      ftrace_events: "power/wakeup_source_deactivate"
      ftrace_events: "sched/sched_blocked_reason"
      ftrace_events: "sched/sched_process_exit"
      ftrace_events: "sched/sched_process_free"
      ftrace_events: "sched/sched_switch"
      ftrace_events: "sched/sched_wakeup"
      ftrace_events: "sched/sched_wakeup_new"
      ftrace_events: "sched/sched_waking"
      ftrace_events: "task/task_newtask"
      ftrace_events: "task/task_rename"
      ftrace_events: "vmscan/*"
      ftrace_events: "workqueue/*"'''

DEFAULT_CONFIG_9000_DUR_MS = f'''\
{COMMON_DEFAULT_CONFIG_BEGINNING_STRING_1}
{CPUFREQ_STRING_NEW_ANDROID}
{COMMON_DEFAULT_CONFIG_BEGINNING_STRING_2}
{COMMON_DEFAULT_FTRACE_EVENTS}
{COMMON_DEFAULT_CONFIG_MIDDLE_STRING}
duration_ms: {TEST_DUR_MS}
{COMMON_CONFIG_ENDING_STRING}EOF'''

DEFAULT_CONFIG_EXCLUDED_FTRACE_EVENTS = f'''\
{COMMON_DEFAULT_CONFIG_BEGINNING_STRING_1}
{CPUFREQ_STRING_NEW_ANDROID}
{COMMON_DEFAULT_CONFIG_BEGINNING_STRING_2}
      ftrace_events: "dmabuf_heap/dma_heap_stat"
      ftrace_events: "ftrace/print"
      ftrace_events: "gpu_mem/gpu_mem_total"
      ftrace_events: "ion/ion_stat"
      ftrace_events: "kmem/ion_heap_grow"
      ftrace_events: "kmem/ion_heap_shrink"
      ftrace_events: "kmem/rss_stat"
      ftrace_events: "lowmemorykiller/lowmemory_kill"
      ftrace_events: "oom/mark_victim"
      ftrace_events: "oom/oom_score_adj_update"
      ftrace_events: "power/cpu_frequency"
      ftrace_events: "power/cpu_idle"
      ftrace_events: "power/gpu_frequency"
      ftrace_events: "power/wakeup_source_activate"
      ftrace_events: "power/wakeup_source_deactivate"
      ftrace_events: "sched/sched_blocked_reason"
      ftrace_events: "sched/sched_process_exit"
      ftrace_events: "sched/sched_process_free"
      ftrace_events: "sched/sched_switch"
      ftrace_events: "sched/sched_wakeup"
      ftrace_events: "sched/sched_wakeup_new"
      ftrace_events: "sched/sched_waking"
      ftrace_events: "task/task_newtask"
      ftrace_events: "task/task_rename"
      ftrace_events: "vmscan/*"
      ftrace_events: "workqueue/*"
{COMMON_DEFAULT_CONFIG_MIDDLE_STRING}
duration_ms: {DEFAULT_DUR_MS}
{COMMON_CONFIG_ENDING_STRING}EOF'''

DEFAULT_CONFIG_INCLUDED_FTRACE_EVENTS = f'''\
{COMMON_DEFAULT_CONFIG_BEGINNING_STRING_1}
{CPUFREQ_STRING_NEW_ANDROID}
{COMMON_DEFAULT_CONFIG_BEGINNING_STRING_2}
      ftrace_events: "dmabuf_heap/dma_heap_stat"
      ftrace_events: "ftrace/print"
      ftrace_events: "gpu_mem/gpu_mem_total"
      ftrace_events: "ion/ion_stat"
      ftrace_events: "kmem/ion_heap_grow"
      ftrace_events: "kmem/ion_heap_shrink"
      ftrace_events: "kmem/rss_stat"
      ftrace_events: "lowmemorykiller/lowmemory_kill"
      ftrace_events: "mm_event/mm_event_record"
      ftrace_events: "oom/mark_victim"
      ftrace_events: "oom/oom_score_adj_update"
      ftrace_events: "power/cpu_frequency"
      ftrace_events: "power/cpu_idle"
      ftrace_events: "power/gpu_frequency"
      ftrace_events: "power/suspend_resume"
      ftrace_events: "power/wakeup_source_activate"
      ftrace_events: "power/wakeup_source_deactivate"
      ftrace_events: "sched/sched_blocked_reason"
      ftrace_events: "sched/sched_process_exit"
      ftrace_events: "sched/sched_process_free"
      ftrace_events: "sched/sched_switch"
      ftrace_events: "sched/sched_wakeup"
      ftrace_events: "sched/sched_wakeup_new"
      ftrace_events: "sched/sched_waking"
      ftrace_events: "task/task_newtask"
      ftrace_events: "task/task_rename"
      ftrace_events: "vmscan/*"
      ftrace_events: "workqueue/*"
      ftrace_events: "mock_ftrace_event1"
      ftrace_events: "mock_ftrace_event2"
{COMMON_DEFAULT_CONFIG_MIDDLE_STRING}
duration_ms: {DEFAULT_DUR_MS}
{COMMON_CONFIG_ENDING_STRING}EOF'''

DEFAULT_CONFIG_OLD_ANDROID = f'''\
{COMMON_DEFAULT_CONFIG_BEGINNING_STRING_1}

{COMMON_DEFAULT_CONFIG_BEGINNING_STRING_2}
{COMMON_DEFAULT_FTRACE_EVENTS}
{COMMON_DEFAULT_CONFIG_MIDDLE_STRING}
duration_ms: {DEFAULT_DUR_MS}
{COMMON_CONFIG_ENDING_STRING}EOF'''

COMMON_CUSTOM_CONFIG_BEGINNING_STRING = f'''\

buffers: {{
  size_kb: 4096
  fill_policy: RING_BUFFER
}}

data_sources: {{
  config {{
    name: "linux.ftrace"
    target_buffer: 2
    ftrace_config {{
      ftrace_events: "dmabuf_heap/dma_heap_stat"
      atrace_categories: "aidl"
      atrace_apps: "lmkd"
      buffer_size_kb: 16384
      drain_period_ms: 150
      symbolize_ksyms: true
    }}
  }}
}}'''

CUSTOM_CONFIG_9000_DUR_MS = f'''\
{COMMON_CUSTOM_CONFIG_BEGINNING_STRING}
duration_ms: {TEST_DUR_MS}
{COMMON_CONFIG_ENDING_STRING}'''

CUSTOM_CONFIG_9000_DUR_MS_WITH_WHITE_SPACE = f'''\
{COMMON_CUSTOM_CONFIG_BEGINNING_STRING}
duration_ms:                                               {TEST_DUR_MS}
{COMMON_CONFIG_ENDING_STRING}'''

CUSTOM_CONFIG_INVALID_DUR_MS = f'''\
{COMMON_CUSTOM_CONFIG_BEGINNING_STRING}
duration_ms: {INVALID_DUR_MS}
{COMMON_CONFIG_ENDING_STRING}'''

CUSTOM_CONFIG_NO_DUR_MS = f'''\
{COMMON_CUSTOM_CONFIG_BEGINNING_STRING}
{COMMON_CONFIG_ENDING_STRING}'''


class ConfigBuilderUnitTest(unittest.TestCase):

  def setUp(self):
    self.command = ProfilerCommand(
        None, "custom", None, None, DEFAULT_DUR_MS, None, None, "test-path",
        None, None, None, None, None, None, None)

  def test_build_default_config_setting_valid_dur_ms(self):
    self.command.dur_ms = TEST_DUR_MS

    config, error = build_default_config(self.command, ANDROID_SDK_VERSION_T)

    self.assertEqual(error, None)
    self.assertEqual(config, DEFAULT_CONFIG_9000_DUR_MS)

  def test_build_default_config_on_old_android_version(self):
    config, error = build_default_config(self.command, ANDROID_SDK_VERSION_S_V2)

    self.assertEqual(error, None)
    self.assertEqual(config, DEFAULT_CONFIG_OLD_ANDROID)

  def test_build_default_config_setting_invalid_dur_ms(self):
    self.command.dur_ms = None

    with self.assertRaises(ValueError) as e:
      build_default_config(self.command, ANDROID_SDK_VERSION_T)

    self.assertEqual(str(e.exception), ("Cannot create config because a valid"
                                        " dur_ms was not set."))

  def test_build_default_config_removing_valid_excluded_ftrace_events(self):
    self.command.excluded_ftrace_events = ["power/suspend_resume",
                                           "mm_event/mm_event_record"]

    config, error = build_default_config(self.command, ANDROID_SDK_VERSION_T)

    self.assertEqual(error, None)
    self.assertEqual(config, DEFAULT_CONFIG_EXCLUDED_FTRACE_EVENTS)

  def test_build_default_config_removing_invalid_excluded_ftrace_events(self):
    self.command.excluded_ftrace_events = ["invalid_ftrace_event"]

    config, error = build_default_config(self.command, ANDROID_SDK_VERSION_T)

    self.assertEqual(config, None)
    self.assertNotEqual(error, None)
    self.assertEqual(error.message, ("Cannot remove ftrace event %s from config"
                                     " because it is not one of the config's"
                                     " ftrace events." %
                                     self.command.excluded_ftrace_events[0]
                                     ))
    self.assertEqual(error.suggestion, ("Please specify one of the following"
                                        " possible ftrace events:\n\t"
                                        " dmabuf_heap/dma_heap_stat\n\t"
                                        " ftrace/print\n\t"
                                        " gpu_mem/gpu_mem_total\n\t"
                                        " ion/ion_stat\n\t"
                                        " kmem/ion_heap_grow\n\t"
                                        " kmem/ion_heap_shrink\n\t"
                                        " kmem/rss_stat\n\t"
                                        " lowmemorykiller/lowmemory_kill\n\t"
                                        " mm_event/mm_event_record\n\t"
                                        " oom/mark_victim\n\t"
                                        " oom/oom_score_adj_update\n\t"
                                        " power/cpu_frequency\n\t"
                                        " power/cpu_idle\n\t"
                                        " power/gpu_frequency\n\t"
                                        " power/suspend_resume\n\t"
                                        " power/wakeup_source_activate\n\t"
                                        " power/wakeup_source_deactivate\n\t"
                                        " sched/sched_blocked_reason\n\t"
                                        " sched/sched_process_exit\n\t"
                                        " sched/sched_process_free\n\t"
                                        " sched/sched_switch\n\t"
                                        " sched/sched_wakeup\n\t"
                                        " sched/sched_wakeup_new\n\t"
                                        " sched/sched_waking\n\t"
                                        " task/task_newtask\n\t"
                                        " task/task_rename\n\t"
                                        " vmscan/*\n\t"
                                        " workqueue/*"))

  def test_build_default_config_adding_valid_included_ftrace_events(self):
    self.command.included_ftrace_events = ["mock_ftrace_event1",
                                           "mock_ftrace_event2"]

    config, error = build_default_config(self.command, ANDROID_SDK_VERSION_T)

    self.assertEqual(error, None)
    self.assertEqual(config, DEFAULT_CONFIG_INCLUDED_FTRACE_EVENTS)

  def test_build_default_config_adding_invalid_included_ftrace_events(self):
    self.command.included_ftrace_events = ["power/suspend_resume"]

    config, error = build_default_config(self.command, ANDROID_SDK_VERSION_T)

    self.assertEqual(config, None)
    self.assertNotEqual(error, None)
    self.assertEqual(error.message, ("Cannot add ftrace event %s to config"
                                     " because it is already one of the"
                                     " config's ftrace events." %
                                     self.command.included_ftrace_events[0]
                                     ))
    self.assertEqual(error.suggestion, ("Please do not specify any of the"
                                        " following ftrace events that are"
                                        " already included:\n\t"
                                        " dmabuf_heap/dma_heap_stat\n\t"
                                        " ftrace/print\n\t"
                                        " gpu_mem/gpu_mem_total\n\t"
                                        " ion/ion_stat\n\t"
                                        " kmem/ion_heap_grow\n\t"
                                        " kmem/ion_heap_shrink\n\t"
                                        " kmem/rss_stat\n\t"
                                        " lowmemorykiller/lowmemory_kill\n\t"
                                        " mm_event/mm_event_record\n\t"
                                        " oom/mark_victim\n\t"
                                        " oom/oom_score_adj_update\n\t"
                                        " power/cpu_frequency\n\t"
                                        " power/cpu_idle\n\t"
                                        " power/gpu_frequency\n\t"
                                        " power/suspend_resume\n\t"
                                        " power/wakeup_source_activate\n\t"
                                        " power/wakeup_source_deactivate\n\t"
                                        " sched/sched_blocked_reason\n\t"
                                        " sched/sched_process_exit\n\t"
                                        " sched/sched_process_free\n\t"
                                        " sched/sched_switch\n\t"
                                        " sched/sched_wakeup\n\t"
                                        " sched/sched_wakeup_new\n\t"
                                        " sched/sched_waking\n\t"
                                        " task/task_newtask\n\t"
                                        " task/task_rename\n\t"
                                        " vmscan/*\n\t"
                                        " workqueue/*"))

  @mock.patch("builtins.open", mock.mock_open(
      read_data=CUSTOM_CONFIG_9000_DUR_MS))
  def test_build_custom_config_extracting_valid_dur_ms(self):
    config, error = build_custom_config(self.command)

    self.assertEqual(error, None)
    self.assertEqual(config, f"<<EOF\n\n{CUSTOM_CONFIG_9000_DUR_MS}\n\n\nEOF")
    self.assertEqual(self.command.dur_ms, TEST_DUR_MS)

  @mock.patch("builtins.open", mock.mock_open(
      read_data=CUSTOM_CONFIG_9000_DUR_MS_WITH_WHITE_SPACE))
  def test_build_custom_config_extracting_valid_dur_ms_with_white_space(self):
    config, error = build_custom_config(self.command)

    self.assertEqual(error, None)
    self.assertEqual(config, (
        f"<<EOF\n\n{CUSTOM_CONFIG_9000_DUR_MS_WITH_WHITE_SPACE}\n\n\nEOF"))
    self.assertEqual(self.command.dur_ms, TEST_DUR_MS)

  @mock.patch("builtins.open", mock.mock_open(
      read_data=CUSTOM_CONFIG_INVALID_DUR_MS))
  def test_build_custom_config_extracting_invalid_dur_ms_error(self):
    config, error = build_custom_config(self.command)

    self.assertNotEqual(error, None)
    self.assertEqual(config, None)
    self.assertEqual(error.message,
                     ("Failed to parse custom perfetto-config on local file"
                      " path: %s. Invalid duration_ms field in config."
                      % self.command.perfetto_config))
    self.assertEqual(error.suggestion,
                     ("Make sure the perfetto config passed via arguments has a"
                      " valid duration_ms value."))

  @mock.patch("builtins.open", mock.mock_open(
      read_data=CUSTOM_CONFIG_NO_DUR_MS))
  def test_build_custom_config_injecting_dur_ms(self):
    duration_string = "duration_ms: " + str(self.command.dur_ms)

    config, error = build_custom_config(self.command)

    self.assertEqual(error, None)
    self.assertEqual(config, (
        f"<<EOF\n\n{CUSTOM_CONFIG_NO_DUR_MS}\n{duration_string}\n\nEOF"))

  @mock.patch.object(builtins, "open")
  def test_build_custom_config_parsing_error(self, mock_open_file):
    self.command.dur_ms = None
    mock_open_file.side_effect = Exception(TEST_FAILURE_MSG)

    config, error = build_custom_config(self.command)

    self.assertNotEqual(error, None)
    self.assertEqual(config, None)
    self.assertEqual(error.message, ("Failed to parse custom perfetto-config on"
                                     " local file path: %s. %s"
                                     % (self.command.perfetto_config,
                                        TEST_FAILURE_MSG)))
    self.assertEqual(error.suggestion, None)


if __name__ == '__main__':
  unittest.main()
