#!/usr/bin/env python
#
#  svnfsfs_tests.py:  testing the 'svnfsfs' tool.
#
#  Subversion is a tool for revision control.
#  See http://subversion.apache.org for more information.
#
# ====================================================================
#    Licensed to the Apache Software Foundation (ASF) under one
#    or more contributor license agreements.  See the NOTICE file
#    distributed with this work for additional information
#    regarding copyright ownership.  The ASF licenses this file
#    to you 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.
######################################################################

# General modules
import os
import logging
import re
import shutil
import sys
import threading
import time
import gzip

logger = logging.getLogger()

# Our testing module
import svntest
from svntest.verify import SVNExpectedStdout, SVNExpectedStderr
from svntest.verify import SVNUnexpectedStderr
from svntest.verify import UnorderedOutput
from svntest.main import SVN_PROP_MERGEINFO

# (abbreviation)
Skip = svntest.testcase.Skip_deco
SkipUnless = svntest.testcase.SkipUnless_deco
XFail = svntest.testcase.XFail_deco
Issues = svntest.testcase.Issues_deco
Issue = svntest.testcase.Issue_deco
Wimp = svntest.testcase.Wimp_deco
SkipDumpLoadCrossCheck = svntest.testcase.SkipDumpLoadCrossCheck_deco
Item = svntest.wc.StateItem

#----------------------------------------------------------------------

# How we currently test 'svnfsfs' --
#
#   'svnfsfs stats':      Run this on a greek repo, then verify that the
#                         various sections are present. The section contents
#                         is matched against section-specific patterns.
#
#   'svnfsfs dump-index': Tested implicitly by the load-index test
#
#   'svnfsfs load-index': Create a greek repo but set shard to 2 and pack
#                         it so we can load into a packed shard with more
#                         than one revision to test ordering issues etc.
#                         r1 also contains a non-trival number of items such
#                         that parser issues etc. have a chance to surface.
#
#                         The idea is dump the index of the pack, mess with
#                         it to cover lots of UI guarantees but keep the
#                         semantics of the relevant bits. Then feed it back
#                         to load-index and verify that the result is still
#                         a complete, consistent etc. repo.
#
######################################################################
# Helper routines

def patch_format(repo_dir, shard_size):
  """Rewrite the format of the FSFS repository REPO_DIR so
  that it would use sharding with SHARDS revisions per shard."""

  format_path = os.path.join(repo_dir, "db", "format")
  contents = open(format_path, 'rb').read()
  processed_lines = []

  for line in contents.split(b"\n"):
    if line.startswith(b"layout "):
      processed_lines.append(b"layout sharded %d" % shard_size)
    else:
      processed_lines.append(line)

  new_contents = b"\n".join(processed_lines)
  os.chmod(format_path, svntest.main.S_ALL_RW)
  with open(format_path, 'wb') as f:
    f.write(new_contents)

######################################################################
# Tests

#----------------------------------------------------------------------

@SkipUnless(svntest.main.is_fs_type_fsfs)
def test_stats(sbox):
  "stats output"

  sbox.build(create_wc=False)

  exit_code, output, errput = \
    svntest.actions.run_and_verify_svnfsfs(None, [], 'stats', sbox.repo_dir)

  # split output into sections
  sections = { }

  last_line = ''
  section_name = ''
  section_contents = []
  for line in output:
    line = line.rstrip()
    if line != '':

      # If the first character is not a space, then LINE is a section header
      if line[0] == ' ':
        section_contents.append(line)
      else:

        # Store previous section
        if section_name != '':
          sections[section_name] = section_contents

          # Is the output formatted nicely?
          if last_line != '':
            logger.warn("Error: no empty line before section '" + line + "'")
            raise svntest.Failure

        # start new section
        section_name = line
        section_contents = []

    last_line = line

  sections[section_name] = section_contents

  # verify that these sections exist
  sections_to_find = ['Reading revisions',
                      'Global statistics:',
                      'Noderev statistics:',
                      'Representation statistics:',
                      'Directory representation statistics:',
                      'File representation statistics:',
                      'Directory property representation statistics:',
                      'File property representation statistics:',
                      'Largest representations:',
                      'Extensions by number of representations:',
                      'Extensions by size of changed files:',
                      'Extensions by size of representations:',
                      'Histogram of expanded node sizes:',
                      'Histogram of representation sizes:',
                      'Histogram of file sizes:',
                      'Histogram of file representation sizes:',
                      'Histogram of file property sizes:',
                      'Histogram of file property representation sizes:',
                      'Histogram of directory sizes:',
                      'Histogram of directory representation sizes:',
                      'Histogram of directory property sizes:',
                      'Histogram of directory property representation sizes:']
  patterns_to_find = {
    'Reading revisions' : ['\s+ 0[ 0-9]*'],
    'Global .*'         : ['.*\d+ bytes in .*\d+ revisions',
                           '.*\d+ bytes in .*\d+ changes',
                           '.*\d+ bytes in .*\d+ node revision records',
                           '.*\d+ bytes in .*\d+ representations',
                           '.*\d+ bytes expanded representation size',
                           '.*\d+ bytes with rep-sharing off' ],
    'Noderev .*'        : ['.*\d+ bytes in .*\d+ nodes total',
                           '.*\d+ bytes in .*\d+ directory noderevs',
                           '.*\d+ bytes in .*\d+ file noderevs' ],
    'Representation .*' : ['.*\d+ bytes in .*\d+ representations total',
                           '.*\d+ bytes in .*\d+ directory representations',
                           '.*\d+ bytes in .*\d+ file representations',
                           '.*\d+ bytes in .*\d+ representations of added file nodes',
                           '.*\d+ bytes in .*\d+ directory property representations',
                           '.*\d+ bytes in .*\d+ file property representations',
                           '.*\d+ average delta chain length',
                           '.*\d+ bytes in header & footer overhead' ],
    '.* representation statistics:' :
                          ['.*\d+ bytes in .*\d+ reps',
                           '.*\d+ bytes in .*\d+ shared reps',
                           '.*\d+ bytes expanded size',
                           '.*\d+ bytes expanded shared size',
                           '.*\d+ bytes with rep-sharing off',
                           '.*\d+ shared references',
                           '.*\d+ average delta chain length'],
    'Largest.*:'        : ['.*\d+ r\d+ */\S*'],
    'Extensions by number .*:' :
                          ['.*\d+ \( ?\d+%\) representations'],
    'Extensions by size .*:' :
                          ['.*\d+ \( ?\d+%\) bytes'],
    'Histogram of .*:'  : ['.*\d+ \.\. < \d+.*\d+ \( ?\d+%\) bytes in *\d+ \( ?\d+%\) items']
  }

  # check that the output contains all sections
  for section_name in sections_to_find:
    if not section_name in sections.keys():
      logger.warn("Error: section '" + section_name + "' not found")
      raise svntest.Failure

  # check section contents
  for section_name in sections.keys():
    patterns = []

    # find the suitable patterns for the current section
    for pattern in patterns_to_find.keys():
      if re.match(pattern, section_name):
        patterns = patterns_to_find[pattern]
        break;

    if len(patterns) == 0:
      logger.warn("Error: unexpected section '" + section_name + "' found'")
      logger.warn(sections[section_name])
      raise svntest.Failure

    # each line in the section must match one of the patterns
    for line in sections[section_name]:
      found = False

      for pattern in patterns:
        if re.match(pattern, line):
          found = True
          break

      if not found:
        logger.warn("Error: unexpected line '" + line + "' in section '"
                    + section_name + "'")
        logger.warn(sections[section_name])
        raise svntest.Failure

#----------------------------------------------------------------------

@SkipUnless(svntest.main.is_fs_type_fsfs)
@SkipUnless(svntest.main.fs_has_pack)
@SkipUnless(svntest.main.is_fs_log_addressing)
def load_index_sharded(sbox):
  "load-index in a packed repo"

  # Configure two files per shard to trigger packing.
  sbox.build(create_wc=False)
  patch_format(sbox.repo_dir, shard_size=2)

  expected_output = ["Packing revisions in shard 0...done.\n"]
  svntest.actions.run_and_verify_svnadmin(expected_output, [],
                                          "pack", sbox.repo_dir)

  # Read P2L index using svnfsfs.
  exit_code, items, errput = \
    svntest.actions.run_and_verify_svnfsfs(None, [], "dump-index", "-r0",
                                           sbox.repo_dir)

  # load-index promises to deal with input that
  #
  # * uses the same encoding as the dump-index output
  # * is not in ascending item offset order
  # * contains lines with the full table header
  # * invalid or incorrect data in the checksum column and beyond
  # * starts with an item which does not belong to the first revision
  #   in the pack file
  #
  # So, let's mess with the ITEMS list to call in on these promises.

  # not in ascending order
  items.reverse()

  # multiple headers (there is already one now at the bottom)
  items.insert(0, "       Start       Length Type   Revision     Item Checksum\n")

  # make columns have a variable size
  # mess with the checksums
  # add a junk column
  # keep header lines as are
  for i in range(0, len(items)):
    if items[i].find("Start") == -1:
      columns = items[i].split()
      columns[5] = columns[5].replace('f','-').replace('0','9')
      columns.append("junk")
      items[i] = ' '.join(columns) + "\n"

  # first entry shall be for rev 1, pack starts at rev 0, though
  for i in range(0, len(items)):
    if items[i].split()[3] == "1":
      if i != 1:
        items[i],items[1] = items[1],items[i]
      break

  assert(items[1].split()[3] == "1")

  # The STDIN data must be binary.
  items = svntest.main.ensure_list(map(str.encode, items))

  # Reload the index
  exit_code, output, errput = svntest.main.run_command_stdin(
    svntest.main.svnfsfs_binary, [], 0, False, items,
    "load-index", sbox.repo_dir)

  # Run verify to see whether we broke anything.
  expected_output = ["* Verifying metadata at revision 0 ...\n",
                     "* Verifying repository metadata ...\n",
                     "* Verified revision 0.\n",
                     "* Verified revision 1.\n"]
  svntest.actions.run_and_verify_svnadmin(expected_output, [],
                                          "verify", sbox.repo_dir)

@SkipUnless(svntest.main.is_fs_type_fsfs)
def test_stats_on_empty_repo(sbox):
  "stats on empty repo shall not crash"

  sbox.build(create_wc=False, empty=True)

  exit_code, output, errput = \
    svntest.actions.run_and_verify_svnfsfs(None, [], 'stats', sbox.repo_dir)

########################################################################
# Run the tests


# list all tests here, starting with None:
test_list = [ None,
              test_stats,
              load_index_sharded,
              test_stats_on_empty_repo,
             ]

if __name__ == '__main__':
  svntest.main.run_tests(test_list)
  # NOTREACHED


### End of file.
