#    Copyright 2015-2017 ARM Limited, Google and contributors
#
# 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.
#
from __future__ import unicode_literals
from __future__ import division
from __future__ import print_function

from builtins import chr
import os
import json
import shutil
import sys
import unittest
import utils_tests
import trappy
from trappy.ftrace import GenericFTrace
from trappy.systrace import SysTrace

class TestCaching(utils_tests.SetupDirectory):
    def __init__(self, *args, **kwargs):
        super(TestCaching, self).__init__(
            [("trace_sched.txt", "trace.txt"),
             ("trace_sched.txt", "trace.raw.txt"),
             ("trace_systrace.html", "trace.html")],
            *args,
            **kwargs)

    def test_cache_created(self):
        """Test cache creation when enabled"""
        GenericFTrace.disable_cache = False
        traces = (trappy.FTrace(), trappy.SysTrace(path='./trace.html'))

        for trace in traces:
            trace_path = os.path.abspath(trace.trace_path)
            trace_dir = os.path.dirname(trace_path)
            trace_file = os.path.basename(trace_path)
            cache_dir = '.' + trace_file + '.cache'

            self.assertTrue(cache_dir in os.listdir(trace_dir))

    def test_cache_not_created(self):
        """Test that cache should not be created when disabled """
        GenericFTrace.disable_cache = True
        traces = (trappy.FTrace(), trappy.SysTrace(path='./trace.html'))

        for trace in traces:
            trace_path = os.path.abspath(trace.trace_path)
            trace_dir = os.path.dirname(trace_path)
            trace_file = os.path.basename(trace_path)
            cache_dir = '.' + trace_file + '.cache'

            self.assertFalse(cache_dir in os.listdir(trace_dir))

    def test_compare_cached_vs_uncached(self):
        """ Test that the cached and uncached traces are same """
        # Build the cache, but the actual trace will be parsed
        # fresh since this is a first time parse
        GenericFTrace.disable_cache = False
        uncached_trace = trappy.FTrace()
        uncached_dfr = uncached_trace.sched_wakeup.data_frame

        # Now read from previously parsed cache by reusing the path
        cached_trace = trappy.FTrace(uncached_trace.trace_path)
        cached_dfr = cached_trace.sched_wakeup.data_frame

        # By default, the str to float conversion done when reading from csv is
        # different from the one used when reading from the trace.txt file.
        #
        # Here's an example:
        # - trace.txt string timestamps:
        #   [76.402065, 80.402065, 80.001337]
        # - parsed dataframe timestamps:
        #   [76.402065000000007, 80.402065000000007, 82.001337000000007]
        #
        # - csv string timestamps:
        #   [76.402065, 80.402065, 80.001337]
        # - cached dataframe timestamps:
        #   [76.402064999999993, 80.402064999999993, 82.001337000000007]
        #
        # To fix this, the timestamps read from the cache are converted using
        # the same conversion method as the trace.txt parser, which results in
        # cache-read timestamps being identical to trace-read timestamps.
        #
        # This test ensures that this stays true.

        cached_times = [r[0] for r in cached_dfr.iterrows()]
        uncached_times = [r[0] for r in uncached_dfr.iterrows()]

        self.assertTrue(cached_times == uncached_times)

        # compare other columns as well
        self.assertTrue([r[1].pid for r in cached_dfr.iterrows()] ==
                        [r[1].pid for r in uncached_dfr.iterrows()])

        self.assertTrue([r[1].comm for r in cached_dfr.iterrows()] ==
                        [r[1].comm for r in uncached_dfr.iterrows()])

        self.assertTrue([r[1].prio for r in cached_dfr.iterrows()] ==
                        [r[1].prio for r in uncached_dfr.iterrows()])

    def test_invalid_cache_overwritten(self):
        """Test a cache with a bad checksum is overwritten"""
        # This is a directory so we can't use the files_to_copy arg of
        # SetUpDirectory, just do it ourselves.
        cache_path = ".trace.txt.cache"
        src = os.path.join(utils_tests.TESTS_DIRECTORY, "trace_sched.txt.cache")
        shutil.copytree(src, cache_path)

        metadata_path = os.path.join(cache_path, "metadata.json")

        def read_metadata():
            with open(metadata_path, "r") as f:
                return json.load(f)

        def write_md5(md5):
            metadata = read_metadata()
            metadata["md5sum"] = md5
            with open(metadata_path, "w") as f:
                json.dump(metadata, f)


        # Change 1 character of the stored checksum
        md5sum = read_metadata()["md5sum"]
        md5sum_inc = md5sum[:-1] + chr(ord(md5sum[-1]) + 1)
        write_md5(md5sum_inc)

        # Parse a trace, this should delete and overwrite the invalidated cache
        GenericFTrace.disable_cache = False
        trace = trappy.FTrace()

        # Check that the modified md5sum was overwritten
        self.assertNotEqual(read_metadata()["md5sum"], md5sum_inc,
                            "The invalid ftrace cache wasn't overwritten")

    def test_cache_dynamic_events(self):
        """Test that caching works if new event parsers have been registered"""

        # Parse the trace to create a cache
        GenericFTrace.disable_cache = False
        trace1 = trappy.FTrace()

        # Check we're actually testing what we think we are
        if hasattr(trace1, 'dynamic_event'):
            raise RuntimeError('Test bug: found unexpected event in trace')

        # Now register a new event type, call the constructor again, and check
        # that the newly added event (which is not present in the cache) is
        # parsed.

        parse_class = trappy.register_dynamic_ftrace("DynamicEvent", "dynamic_test_key")

        trace2 = trappy.FTrace()
        self.assertTrue(len(trace2.dynamic_event.data_frame) == 1)

        trappy.unregister_dynamic_ftrace(parse_class)

    def test_cache_normalize_time(self):
        """Test that caching doesn't break normalize_time"""
        GenericFTrace.disable_cache = False

        # Times in trace_sched.txt
        start_time = 6550.018511
        first_freq_event_time = 6550.056870

        # Parse without normalizing time
        trace1 = trappy.FTrace(events=['cpu_frequency', 'sched_wakeup'],
                               normalize_time=False)

        self.assertEqual(trace1.cpu_frequency.data_frame.index[0],
                         first_freq_event_time)

        # Parse with normalized time
        trace2 = trappy.FTrace(events=['cpu_frequency', 'sched_wakeup'],
                               normalize_time=True)

        self.assertEqual(trace2.cpu_frequency.data_frame.index[0],
                         first_freq_event_time - start_time)

    def test_cache_window_broad(self):
        """Test that caching doesn't break the 'window' parameter"""
        GenericFTrace.disable_cache = False

        trace1 = trappy.FTrace(
            events=['sched_wakeup'],
            window=(0, 1))

        # Check that we're testing what we think we're testing The trace
        # contains 2 sched_wakeup events; this window should get rid of one of
        # them.
        if len(trace1.sched_wakeup.data_frame) != 1:
            raise RuntimeError('Test bug: bad sched_wakeup event count')

        # Parse again without the window
        trace1 = trappy.FTrace(
            events=['sched_wakeup'],
            window=(0, None))

        self.assertEqual(len(trace1.sched_wakeup.data_frame), 2)

    def test_cache_window_narrow(self):
        """
        Test that applying a window to a cached trace returns EXACTLY what is expected
        """
        # As described in test_compare_cache_vs_uncached, reading from cache
        # results in slightly different timestamps
        #
        # This test verifies that applying windows results in identical
        # dataframes whether cache is used or not.
        GenericFTrace.disable_cache = False

        uncached_trace = trappy.FTrace()

        trace = trappy.FTrace(uncached_trace.trace_path,
                              normalize_time=False,
                              abs_window=(6550.100000, 6552.000002))

        self.assertAlmostEquals(trace.get_duration(), 1.900002)

        self.assertEqual(len(trace.sched_wakeup.data_frame), 2)
        self.assertEqual(len(trace.sched_wakeup_new.data_frame), 1)

    def test_ftrace_metadata(self):
        """Test that caching keeps trace metadata"""
        GenericFTrace.disable_cache = False

        self.test_cache_created()

        trace = trappy.FTrace()

        version = int(trace._version)
        cpus = int(trace._cpus)

        self.assertEqual(version, 6)
        self.assertEqual(cpus, 6)

    def test_cache_delete_single(self):
        GenericFTrace.disable_cache = False
        trace = trappy.FTrace()

        trace_path = os.path.abspath(trace.trace_path)
        trace_dir = os.path.dirname(trace_path)
        trace_file = os.path.basename(trace_path)
        cache_dir = '.' + trace_file + '.cache'
        number_of_trace_categories = 31
        self.assertEqual(len(os.listdir(cache_dir)), number_of_trace_categories)

        os.remove(os.path.join(cache_dir, 'SchedWakeup.csv'))
        self.assertEqual(len(os.listdir(cache_dir)), number_of_trace_categories - 1)

        # Generate trace again, should regenerate only the missing item
        trace = trappy.FTrace()
        self.assertEqual(len(os.listdir(cache_dir)), number_of_trace_categories)
        for c in trace.trace_classes:
            if isinstance(c, trace.class_definitions['sched_wakeup']):
                self.assertEqual(c.cached, False)
                continue
            self.assertEqual(c.cached, True)

[((70, 25, 70, 40), 'trappy.FTrace', 'trappy.FTrace', ({}, {}), '()', False, 'import trappy\n'), ((74, 23, 74, 63), 'trappy.FTrace', 'trappy.FTrace', ({(74, 37, 74, 62): 'uncached_trace.trace_path'}, {}), '(uncached_trace.trace_path)', False, 'import trappy\n'), ((117, 14, 117, 80), 'os.path.join', 'os.path.join', ({(117, 27, 117, 54): 'utils_tests.TESTS_DIRECTORY', (117, 56, 117, 79): '"""trace_sched.txt.cache"""'}, {}), "(utils_tests.TESTS_DIRECTORY, 'trace_sched.txt.cache')", False, 'import os\n'), ((118, 8, 118, 40), 'shutil.copytree', 'shutil.copytree', ({(118, 24, 118, 27): 'src', (118, 29, 118, 39): 'cache_path'}, {}), '(src, cache_path)', False, 'import shutil\n'), ((120, 24, 120, 65), 'os.path.join', 'os.path.join', ({(120, 37, 120, 47): 'cache_path', (120, 49, 120, 64): '"""metadata.json"""'}, {}), "(cache_path, 'metadata.json')", False, 'import os\n'), ((140, 16, 140, 31), 'trappy.FTrace', 'trappy.FTrace', ({}, {}), '()', False, 'import trappy\n'), ((151, 17, 151, 32), 'trappy.FTrace', 'trappy.FTrace', ({}, {}), '()', False, 'import trappy\n'), ((161, 22, 161, 88), 'trappy.register_dynamic_ftrace', 'trappy.register_dynamic_ftrace', ({(161, 53, 161, 67): '"""DynamicEvent"""', (161, 69, 161, 87): '"""dynamic_test_key"""'}, {}), "('DynamicEvent', 'dynamic_test_key')", False, 'import trappy\n'), ((163, 17, 163, 32), 'trappy.FTrace', 'trappy.FTrace', ({}, {}), '()', False, 'import trappy\n'), ((166, 8, 166, 53), 'trappy.unregister_dynamic_ftrace', 'trappy.unregister_dynamic_ftrace', ({(166, 41, 166, 52): 'parse_class'}, {}), '(parse_class)', False, 'import trappy\n'), ((177, 17, 178, 52), 'trappy.FTrace', 'trappy.FTrace', (), '', False, 'import trappy\n'), ((184, 17, 185, 51), 'trappy.FTrace', 'trappy.FTrace', (), '', False, 'import trappy\n'), ((194, 17, 196, 26), 'trappy.FTrace', 'trappy.FTrace', (), '', False, 'import trappy\n'), ((205, 17, 207, 29), 'trappy.FTrace', 'trappy.FTrace', (), '', False, 'import trappy\n'), ((222, 25, 222, 40), 'trappy.FTrace', 'trappy.FTrace', ({}, {}), '()', False, 'import trappy\n'), ((224, 16, 226, 68), 'trappy.FTrace', 'trappy.FTrace', (), '', False, 'import trappy\n'), ((239, 16, 239, 31), 'trappy.FTrace', 'trappy.FTrace', ({}, {}), '()', False, 'import trappy\n'), ((249, 16, 249, 31), 'trappy.FTrace', 'trappy.FTrace', ({}, {}), '()', False, 'import trappy\n'), ((251, 21, 251, 54), 'os.path.abspath', 'os.path.abspath', ({(251, 37, 251, 53): 'trace.trace_path'}, {}), '(trace.trace_path)', False, 'import os\n'), ((252, 20, 252, 47), 'os.path.dirname', 'os.path.dirname', ({(252, 36, 252, 46): 'trace_path'}, {}), '(trace_path)', False, 'import os\n'), ((253, 21, 253, 49), 'os.path.basename', 'os.path.basename', ({(253, 38, 253, 48): 'trace_path'}, {}), '(trace_path)', False, 'import os\n'), ((262, 16, 262, 31), 'trappy.FTrace', 'trappy.FTrace', ({}, {}), '()', False, 'import trappy\n'), ((42, 18, 42, 33), 'trappy.FTrace', 'trappy.FTrace', ({}, {}), '()', False, 'import trappy\n'), ((42, 35, 42, 71), 'trappy.SysTrace', 'trappy.SysTrace', (), '', False, 'import trappy\n'), ((45, 25, 45, 58), 'os.path.abspath', 'os.path.abspath', ({(45, 41, 45, 57): 'trace.trace_path'}, {}), '(trace.trace_path)', False, 'import os\n'), ((46, 24, 46, 51), 'os.path.dirname', 'os.path.dirname', ({(46, 40, 46, 50): 'trace_path'}, {}), '(trace_path)', False, 'import os\n'), ((47, 25, 47, 53), 'os.path.basename', 'os.path.basename', ({(47, 42, 47, 52): 'trace_path'}, {}), '(trace_path)', False, 'import os\n'), ((55, 18, 55, 33), 'trappy.FTrace', 'trappy.FTrace', ({}, {}), '()', False, 'import trappy\n'), ((55, 35, 55, 71), 'trappy.SysTrace', 'trappy.SysTrace', (), '', False, 'import trappy\n'), ((58, 25, 58, 58), 'os.path.abspath', 'os.path.abspath', ({(58, 41, 58, 57): 'trace.trace_path'}, {}), '(trace.trace_path)', False, 'import os\n'), ((59, 24, 59, 51), 'os.path.dirname', 'os.path.dirname', ({(59, 40, 59, 50): 'trace_path'}, {}), '(trace_path)', False, 'import os\n'), ((60, 25, 60, 53), 'os.path.basename', 'os.path.basename', ({(60, 42, 60, 52): 'trace_path'}, {}), '(trace_path)', False, 'import os\n'), ((258, 18, 258, 60), 'os.path.join', 'os.path.join', ({(258, 31, 258, 40): 'cache_dir', (258, 42, 258, 59): '"""SchedWakeup.csv"""'}, {}), "(cache_dir, 'SchedWakeup.csv')", False, 'import os\n'), ((124, 23, 124, 35), 'json.load', 'json.load', ({(124, 33, 124, 34): 'f'}, {}), '(f)', False, 'import json\n'), ((130, 16, 130, 38), 'json.dump', 'json.dump', ({(130, 26, 130, 34): 'metadata', (130, 36, 130, 37): 'f'}, {}), '(metadata, f)', False, 'import json\n'), ((256, 29, 256, 50), 'os.listdir', 'os.listdir', ({(256, 40, 256, 49): 'cache_dir'}, {}), '(cache_dir)', False, 'import os\n'), ((259, 29, 259, 50), 'os.listdir', 'os.listdir', ({(259, 40, 259, 49): 'cache_dir'}, {}), '(cache_dir)', False, 'import os\n'), ((263, 29, 263, 50), 'os.listdir', 'os.listdir', ({(263, 40, 263, 49): 'cache_dir'}, {}), '(cache_dir)', False, 'import os\n'), ((50, 41, 50, 62), 'os.listdir', 'os.listdir', ({(50, 52, 50, 61): 'trace_dir'}, {}), '(trace_dir)', False, 'import os\n'), ((63, 42, 63, 63), 'os.listdir', 'os.listdir', ({(63, 53, 63, 62): 'trace_dir'}, {}), '(trace_dir)', False, 'import os\n')]