#!/usr/bin/env python
# Copyright (c) 2012 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

import os
import sys
import unittest

from caching_file_system import CachingFileSystem
from extensions_paths import SERVER2
from file_system import FileNotFoundError, StatInfo
from local_file_system import LocalFileSystem
from mock_file_system import MockFileSystem
from object_store_creator import ObjectStoreCreator
from test_file_system import TestFileSystem
from test_object_store import TestObjectStore


def _CreateLocalFs():
  return LocalFileSystem.Create(SERVER2, 'test_data', 'file_system/')


class CachingFileSystemTest(unittest.TestCase):
  def setUp(self):
    # Use this to make sure that every time _CreateCachingFileSystem is called
    # the underlying object store data is the same, within each test.
    self._object_store_dbs = {}

  def _CreateCachingFileSystem(self, fs, start_empty=False):
    def store_type_constructor(namespace, start_empty=False):
      '''Returns an ObjectStore backed onto test-lifetime-persistent objects
      in |_object_store_dbs|.
      '''
      if namespace not in self._object_store_dbs:
        self._object_store_dbs[namespace] = {}
      db = self._object_store_dbs[namespace]
      if start_empty:
        db.clear()
      return TestObjectStore(namespace, init=db)
    object_store_creator = ObjectStoreCreator(start_empty=start_empty,
                                              store_type=store_type_constructor)
    return CachingFileSystem(fs, object_store_creator)

  def testReadFiles(self):
    file_system = self._CreateCachingFileSystem(
        _CreateLocalFs(), start_empty=False)
    expected = {
      './test1.txt': 'test1\n',
      './test2.txt': 'test2\n',
      './test3.txt': 'test3\n',
    }
    self.assertEqual(
        expected,
        file_system.Read(['./test1.txt', './test2.txt', './test3.txt']).Get())

  def testListDir(self):
    file_system = self._CreateCachingFileSystem(
        _CreateLocalFs(), start_empty=False)
    expected = ['dir/'] + ['file%d.html' % i for i in range(7)]
    file_system._read_cache.Set(
        'list/',
        (expected, file_system.Stat('list/').version))
    self.assertEqual(expected, sorted(file_system.ReadSingle('list/').Get()))

    expected.remove('file0.html')
    file_system._read_cache.Set(
        'list/',
        (expected, file_system.Stat('list/').version))
    self.assertEqual(expected, sorted(file_system.ReadSingle('list/').Get()))

  def testCaching(self):
    test_fs = TestFileSystem({
      'bob': {
        'bob0': 'bob/bob0 contents',
        'bob1': 'bob/bob1 contents',
        'bob2': 'bob/bob2 contents',
        'bob3': 'bob/bob3 contents',
      }
    })
    mock_fs = MockFileSystem(test_fs)
    def create_empty_caching_fs():
      return self._CreateCachingFileSystem(mock_fs, start_empty=True)

    file_system = create_empty_caching_fs()

    # The stat/read should happen before resolving the Future, and resolving
    # the future shouldn't do any additional work.
    get_future = file_system.ReadSingle('bob/bob0')
    self.assertTrue(*mock_fs.CheckAndReset(read_count=1))
    self.assertEqual('bob/bob0 contents', get_future.Get())
    self.assertTrue(*mock_fs.CheckAndReset(read_resolve_count=1, stat_count=1))

    # Resource has been cached, so test resource is not re-fetched.
    self.assertEqual('bob/bob0 contents',
                     file_system.ReadSingle('bob/bob0').Get())
    self.assertTrue(*mock_fs.CheckAndReset())

    # Test if the Stat version is the same the resource is not re-fetched.
    file_system = create_empty_caching_fs()
    self.assertEqual('bob/bob0 contents',
                     file_system.ReadSingle('bob/bob0').Get())
    self.assertTrue(*mock_fs.CheckAndReset(stat_count=1))

    # Test if there is a newer version, the resource is re-fetched.
    file_system = create_empty_caching_fs()
    test_fs.IncrementStat();
    future = file_system.ReadSingle('bob/bob0')
    self.assertTrue(*mock_fs.CheckAndReset(read_count=1, stat_count=1))
    self.assertEqual('bob/bob0 contents', future.Get())
    self.assertTrue(*mock_fs.CheckAndReset(read_resolve_count=1))

    # Test directory and subdirectory stats are cached.
    file_system = create_empty_caching_fs()
    file_system._stat_cache.Del('bob/bob0')
    file_system._read_cache.Del('bob/bob0')
    file_system._stat_cache.Del('bob/bob1')
    test_fs.IncrementStat();
    futures = (file_system.ReadSingle('bob/bob1'),
               file_system.ReadSingle('bob/bob0'))
    self.assertTrue(*mock_fs.CheckAndReset(read_count=2))
    self.assertEqual(('bob/bob1 contents', 'bob/bob0 contents'),
                     tuple(future.Get() for future in futures))
    self.assertTrue(*mock_fs.CheckAndReset(read_resolve_count=2, stat_count=1))
    self.assertEqual('bob/bob1 contents',
                     file_system.ReadSingle('bob/bob1').Get())
    self.assertTrue(*mock_fs.CheckAndReset())

    # Test a more recent parent directory doesn't force a refetch of children.
    file_system = create_empty_caching_fs()
    file_system._read_cache.Del('bob/bob0')
    file_system._read_cache.Del('bob/bob1')
    futures = (file_system.ReadSingle('bob/bob1'),
               file_system.ReadSingle('bob/bob2'),
               file_system.ReadSingle('bob/bob3'))
    self.assertTrue(*mock_fs.CheckAndReset(read_count=3))
    self.assertEqual(
        ('bob/bob1 contents', 'bob/bob2 contents', 'bob/bob3 contents'),
        tuple(future.Get() for future in futures))
    self.assertTrue(*mock_fs.CheckAndReset(read_resolve_count=3, stat_count=1))

    test_fs.IncrementStat(path='bob/bob0')
    file_system = create_empty_caching_fs()
    self.assertEqual('bob/bob1 contents',
                     file_system.ReadSingle('bob/bob1').Get())
    self.assertEqual('bob/bob2 contents',
                     file_system.ReadSingle('bob/bob2').Get())
    self.assertEqual('bob/bob3 contents',
                     file_system.ReadSingle('bob/bob3').Get())
    self.assertTrue(*mock_fs.CheckAndReset(stat_count=1))

    file_system = create_empty_caching_fs()
    file_system._stat_cache.Del('bob/bob0')
    future = file_system.ReadSingle('bob/bob0')
    self.assertTrue(*mock_fs.CheckAndReset(read_count=1))
    self.assertEqual('bob/bob0 contents', future.Get())
    self.assertTrue(*mock_fs.CheckAndReset(read_resolve_count=1, stat_count=1))
    self.assertEqual('bob/bob0 contents',
                     file_system.ReadSingle('bob/bob0').Get())
    self.assertTrue(*mock_fs.CheckAndReset())

    # Test skip_not_found caching behavior.
    file_system = create_empty_caching_fs()
    future = file_system.ReadSingle('bob/no_file', skip_not_found=True)
    self.assertTrue(*mock_fs.CheckAndReset(read_count=1))
    self.assertEqual(None, future.Get())
    self.assertTrue(*mock_fs.CheckAndReset(read_resolve_count=1, stat_count=1))
    future = file_system.ReadSingle('bob/no_file', skip_not_found=True)
    # There shouldn't be another read/stat from the file system;
    # we know the file is not there.
    self.assertTrue(*mock_fs.CheckAndReset())
    future = file_system.ReadSingle('bob/no_file')
    self.assertTrue(*mock_fs.CheckAndReset(read_count=1))
    # Even though we cached information about non-existent files,
    # trying to read one without specifiying skip_not_found should
    # still raise an error.
    self.assertRaises(FileNotFoundError, future.Get)

  def testCachedStat(self):
    test_fs = TestFileSystem({
      'bob': {
        'bob0': 'bob/bob0 contents',
        'bob1': 'bob/bob1 contents'
      }
    })
    mock_fs = MockFileSystem(test_fs)

    file_system = self._CreateCachingFileSystem(mock_fs, start_empty=False)

    self.assertEqual(StatInfo('0'), file_system.Stat('bob/bob0'))
    self.assertTrue(*mock_fs.CheckAndReset(stat_count=1))
    self.assertEqual(StatInfo('0'), file_system.Stat('bob/bob0'))
    self.assertTrue(*mock_fs.CheckAndReset())

    # Caching happens on a directory basis, so reading other files from that
    # directory won't result in a stat.
    self.assertEqual(StatInfo('0'), file_system.Stat('bob/bob1'))
    self.assertEqual(
        StatInfo('0', child_versions={'bob0': '0', 'bob1': '0'}),
        file_system.Stat('bob/'))
    self.assertTrue(*mock_fs.CheckAndReset())

    # Even though the stat is bumped, the object store still has it cached so
    # this won't update.
    test_fs.IncrementStat()
    self.assertEqual(StatInfo('0'), file_system.Stat('bob/bob0'))
    self.assertEqual(StatInfo('0'), file_system.Stat('bob/bob1'))
    self.assertEqual(
        StatInfo('0', child_versions={'bob0': '0', 'bob1': '0'}),
        file_system.Stat('bob/'))
    self.assertTrue(*mock_fs.CheckAndReset())

  def testFreshStat(self):
    test_fs = TestFileSystem({
      'bob': {
        'bob0': 'bob/bob0 contents',
        'bob1': 'bob/bob1 contents'
      }
    })
    mock_fs = MockFileSystem(test_fs)

    def run_expecting_stat(stat):
      def run():
        file_system = self._CreateCachingFileSystem(mock_fs, start_empty=True)
        self.assertEqual(
            StatInfo(stat, child_versions={'bob0': stat, 'bob1': stat}),
            file_system.Stat('bob/'))
        self.assertTrue(*mock_fs.CheckAndReset(stat_count=1))
        self.assertEqual(StatInfo(stat), file_system.Stat('bob/bob0'))
        self.assertEqual(StatInfo(stat), file_system.Stat('bob/bob0'))
        self.assertTrue(*mock_fs.CheckAndReset())
      run()
      run()

    run_expecting_stat('0')
    test_fs.IncrementStat()
    run_expecting_stat('1')

  def testSkipNotFound(self):
    caching_fs = self._CreateCachingFileSystem(TestFileSystem({
      'bob': {
        'bob0': 'bob/bob0 contents',
        'bob1': 'bob/bob1 contents'
      }
    }))
    def read_skip_not_found(paths):
      return caching_fs.Read(paths, skip_not_found=True).Get()
    self.assertEqual({}, read_skip_not_found(('grub',)))
    self.assertEqual({}, read_skip_not_found(('bob/bob2',)))
    self.assertEqual({
      'bob/bob0': 'bob/bob0 contents',
    }, read_skip_not_found(('bob/bob0', 'bob/bob2')))

  def testWalkCaching(self):
    test_fs = TestFileSystem({
      'root': {
        'file1': 'file1',
        'file2': 'file2',
        'dir1': {
          'dir1_file1': 'dir1_file1',
          'dir2': {},
          'dir3': {
            'dir3_file1': 'dir3_file1',
            'dir3_file2': 'dir3_file2'
          }
        }
      }
    })
    mock_fs = MockFileSystem(test_fs)
    file_system = self._CreateCachingFileSystem(mock_fs, start_empty=True)
    for walkinfo in file_system.Walk(''):
      pass
    self.assertTrue(*mock_fs.CheckAndReset(
        read_resolve_count=5, read_count=5, stat_count=5))

    all_dirs, all_files = [], []
    for root, dirs, files in file_system.Walk(''):
      all_dirs.extend(dirs)
      all_files.extend(files)
    self.assertEqual(sorted(['root/', 'dir1/', 'dir2/', 'dir3/']),
                     sorted(all_dirs))
    self.assertEqual(
        sorted(['file1', 'file2', 'dir1_file1', 'dir3_file1', 'dir3_file2']),
        sorted(all_files))
    # All data should be cached.
    self.assertTrue(*mock_fs.CheckAndReset())

    # Starting from a different root should still pull cached data.
    for walkinfo in file_system.Walk('root/dir1/'):
      pass
    self.assertTrue(*mock_fs.CheckAndReset())
    # TODO(ahernandez): Test with a new instance CachingFileSystem so a
    # different object store is utilized.

  def testVersionedStat(self):
    test_fs = TestFileSystem({
      'bob': {
        'bob0': 'bob/bob0 contents',
        'bob1': 'bob/bob1 contents'
      }
    })

    # Create a versioned FileSystem and verify that multiple CachingFileSystem
    # instances wrapping it will share the same stat cache.
    mock_fs = MockFileSystem(test_fs)
    mock_fs.SetVersion('abcdefg')

    def run_and_expect_stat_count(paths, stat_count=0):
      file_system = self._CreateCachingFileSystem(mock_fs, start_empty=True)
      [file_system.Stat(path) for path in paths]
      self.assertTrue(*mock_fs.CheckAndReset(stat_count=stat_count))

    run_and_expect_stat_count(['bob/', 'bob/bob0', 'bob/bob1'], stat_count=1)
    run_and_expect_stat_count(['bob/', 'bob/bob0', 'bob/bob1'], stat_count=0)

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