#!/usr/bin/env python
#
#  svnviewspec_test.py:  testing the 'svn-viewspec.py' tool.
#
#  Execute these tests using 'pytest' (pytest.org):
#    py.test svnviewspec_test.py
#
#  Subversion is a tool for revision control.
#  See https://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.
######################################################################


import sys
import os.path
import tempfile

sys.path.append(os.path.abspath(os.path.realpath(__file__)))

svn_viewspec = __import__("svn-viewspec")
__SCRIPTNAME__ = 'svn-viewspec.py'


#########################################################################
# mock the 'system' call
os_system_lines = []

def my_os_system(str):
    os_system_lines.append(str)

def setup_function(function):
    global os_system_lines
    os_system_lines = []

#########################################################################

def perform_viewspec(args):
    return svn_viewspec.main(my_os_system, [__SCRIPTNAME__] + args)

def test_that_viewspec_does_checkout_for_its_own_inline_example(capsys):

    spec = """Format: 1
Url: http://svn.apache.org/r/a/s
Revision: 36

trunk/**
branches/1.5.x/**
branches/1.6.x/**
README
branches/1.4.x/STATUS
branches/1.4.x/s/tests/cmdline/~
"""

    perform_viewspec(
        ["checkout", (make_temp_file_containing(spec)), "a/b"])
    sys_out_lines, sys_err_lines = capsys.readouterr()

    expected = \
"""svn checkout "http://svn.apache.org/r/a/s" "a/b" --depth=empty --revision=36
svn update "a/b/README" --set-depth=empty --revision=36
svn update "a/b/branches" --set-depth=empty --revision=36
svn update "a/b/branches/1.4.x" --set-depth=empty --revision=36
svn update "a/b/branches/1.4.x/STATUS" --set-depth=empty --revision=36
svn update "a/b/branches/1.4.x/s" --set-depth=empty --revision=36
svn update "a/b/branches/1.4.x/s/tests" --set-depth=empty --revision=36
svn update "a/b/branches/1.4.x/s/tests/cmdline" --set-depth=files --revision=36
svn update "a/b/branches/1.5.x" --set-depth=infinity --revision=36
svn update "a/b/branches/1.6.x" --set-depth=infinity --revision=36
svn update "a/b/trunk" --set-depth=infinity --revision=36"""

    all_the_lines_should_be_the_same(expected, os_system_lines)

    all_the_lines_should_be_the_same([], sys_err_lines)
    all_the_lines_should_be_the_same([], sys_out_lines)


def test_that_viewspec_does_checkout_for_a_tilde_at_root_case(capsys):

    spec = """Format: 1
Url: http://svn.apache.org/repos/asf/svn

~
"""

    perform_viewspec(
        ["checkout", (make_temp_file_containing(spec)), "a/b"])
    sys_out_lines, sys_err_lines = capsys.readouterr()

    expected = \
"""svn checkout "http://svn.apache.org/repos/asf/svn" "a/b" --depth=files"""

    # print ">>>" + "\n".join(os_system_lines) + "<<<"

    all_the_lines_should_be_the_same(expected, os_system_lines)

    all_the_lines_should_be_the_same([], sys_err_lines)
    all_the_lines_should_be_the_same([], sys_out_lines)


def test_that_viewspec_does_checkout_a_splat_at_root_case(capsys):

    spec = """Format: 1
Url: http://svn.apache.org/r/a/svn

*
"""

    perform_viewspec(
        ["checkout", (make_temp_file_containing(spec)), "a/b"])
    sys_out_lines, sys_err_lines = capsys.readouterr()

    expected = \
"""svn checkout "http://svn.apache.org/r/a/svn" "a/b" --depth=immediates"""

    # print ">>>" + "\n".join(os_system_lines) + "<<<"

    all_the_lines_should_be_the_same(expected, os_system_lines)

    all_the_lines_should_be_the_same([], sys_err_lines)
    all_the_lines_should_be_the_same([], sys_out_lines)


def test_that_viewspec_does_checkout_an_infinity_from_root_case(capsys):

    spec = """Format: 1
Url: http://svn.apache.org/r/a/s

**
"""

    perform_viewspec(
        ["checkout", (make_temp_file_containing(spec)), "a/b"])
    sys_out_lines, sys_err_lines = capsys.readouterr()

    expected = \
"""svn checkout "http://svn.apache.org/r/a/s" "a/b" --depth=infinity"""

    # print ">>>" + "\n".join(os_system_lines) + "<<<"

    all_the_lines_should_be_the_same(expected, os_system_lines)

    all_the_lines_should_be_the_same([], sys_err_lines)
    all_the_lines_should_be_the_same([], sys_out_lines)


def test_that_viewspec_does_checkout_for_a_splat_n_tilde_in_a_subdir_case(capsys):
    spec = """Format: 1
Url: http://svn.apache.org/repos/asf/svn

branches/foo/*
branches/bar/~
"""

    perform_viewspec(
      ["checkout", (make_temp_file_containing(spec)), "a/b"])
    sys_out_lines, sys_err_lines = capsys.readouterr()

    expected = \
"""svn checkout "http://svn.apache.org/repos/asf/svn" "a/b" --depth=empty
svn update "a/b/branches" --set-depth=empty
svn update "a/b/branches/bar" --set-depth=files
svn update "a/b/branches/foo" --set-depth=immediates"""

    all_the_lines_should_be_the_same(expected, os_system_lines)

    all_the_lines_should_be_the_same([], sys_err_lines)
    all_the_lines_should_be_the_same([], sys_out_lines)


def test_that_viewspec_does_examine_for_its_own_inline_example2(capsys):

    spec = """Format: 1
Url: http://svn.apache.org/repos/asf/subversion
Revision: 36366

trunk/**
branches/1.5.x/**
branches/1.6.x/**
README
branches/1.4.x/STATUS
branches/1.4.x/subversion/tests/cmdline/~
"""

    perform_viewspec(
        ["examine", (make_temp_file_containing(spec)), "a/b"])
    sys_out_lines, sys_err_lines = capsys.readouterr()

    all_the_lines_should_be_the_same([], os_system_lines)

    expected = \
      """Path:  (depth=empty)
  Path: README (depth=empty)
  Path: branches (depth=empty)
    Path: 1.4.x (depth=empty)
      Path: STATUS (depth=empty)
      Path: subversion (depth=empty)
        Path: tests (depth=empty)
          Path: cmdline (depth=files)
    Path: 1.5.x (depth=infinity)
    Path: 1.6.x (depth=infinity)
  Path: trunk (depth=infinity)"""

    all_the_lines_should_be_the_same(expected, sys_err_lines)

    expected = \
      """Url: http://svn.apache.org/repos/asf/subversion
Revision: 36366

"""

    all_the_lines_should_be_the_same(expected, sys_out_lines)


def test_that_viewspec_prints_help(capsys):

    rc = perform_viewspec(["help"])
    sys_out_lines, sys_err_lines = capsys.readouterr()

    assert 0 == rc

    all_the_lines_should_be_the_same([], sys_err_lines)

    expected = \
      """__SCRIPTNAME__: checkout utility for sparse Subversion working copies

Usage: 1. __SCRIPTNAME__ checkout VIEWSPEC-FILE TARGET-DIR
       2. __SCRIPTNAME__ examine VIEWSPEC-FILE
       3. __SCRIPTNAME__ help
       4. __SCRIPTNAME__ help-format

VIEWSPEC-FILE is the path of a file whose contents describe a
Subversion sparse checkouts layout, or '-' if that description should
be read from stdin.  TARGET-DIR is the working copy directory created
by this script as it checks out the specified layout.

1. Parse VIEWSPEC-FILE and execute the necessary 'svn' command-line
   operations to build out a working copy tree at TARGET-DIR.

2. Parse VIEWSPEC-FILE and dump out a human-readable representation of
   the tree described in the specification.

3. Show this usage message.

4. Show information about the file format this program expects.
      """.replace("__SCRIPTNAME__", __SCRIPTNAME__)

    all_the_lines_should_be_the_same(expected, sys_out_lines)


def test_that_viewspec_prints_unknown_subcommand(capsys):

    rc = perform_viewspec(["helppppppppp"])
    sys_out_lines, sys_err_lines = capsys.readouterr()

    assert 1 == rc

    all_the_lines_should_be_the_same([], sys_out_lines)

    expected = \
      """__SCRIPTNAME__: checkout utility for sparse Subversion working copies

Usage: 1. __SCRIPTNAME__ checkout VIEWSPEC-FILE TARGET-DIR
       2. __SCRIPTNAME__ examine VIEWSPEC-FILE
       3. __SCRIPTNAME__ help
       4. __SCRIPTNAME__ help-format

VIEWSPEC-FILE is the path of a file whose contents describe a
Subversion sparse checkouts layout, or '-' if that description should
be read from stdin.  TARGET-DIR is the working copy directory created
by this script as it checks out the specified layout.

1. Parse VIEWSPEC-FILE and execute the necessary 'svn' command-line
   operations to build out a working copy tree at TARGET-DIR.

2. Parse VIEWSPEC-FILE and dump out a human-readable representation of
   the tree described in the specification.

3. Show this usage message.

4. Show information about the file format this program expects.

ERROR: Unknown subcommand "helppppppppp".""".replace("__SCRIPTNAME__", __SCRIPTNAME__)

    all_the_lines_should_be_the_same(expected, sys_err_lines)


def test_that_viewspec_prints_help_format(capsys):

    rc = perform_viewspec(["help-format"])
    sys_out_lines, sys_err_lines = capsys.readouterr()

    assert 1 == rc

    # print ">>>" + "\n".join(sys_out_lines) + "<<<<"

    expected = \
      """Viewspec File Format
====================

The viewspec file format used by this tool is a collection of headers
(using the typical one-per-line name:value syntax), followed by an
empty line, followed by a set of one-per-line rules.

The headers must contain at least the following:

    Format   - version of the viewspec format used throughout the file
    Url      - base URL applied to all rules; tree checkout location

The following headers are optional:

    Revision - version of the tree items to checkout

Following the headers and blank line separator are the path rules.
The rules are list of URLs -- relative to the base URL stated in the
headers -- with optional annotations to specify the desired working
copy depth of each item:

    PATH/**  - checkout PATH and all its children to infinite depth
    PATH/*   - checkout PATH and its immediate children
    PATH/~   - checkout PATH and its file children
    PATH     - checkout PATH non-recursively

By default, the top-level directory (associated with the base URL) is
checked out with empty depth.  You can override this using the special
rules '**', '*', and '~' as appropriate.

It is not necessary to explicitly list the parent directories of each
path associated with a rule.  If the parent directory of a given path
is not "covered" by a previous rule, it will be checked out with empty
depth.

Examples
========

Here's a sample viewspec file:

    Format: 1
    Url: http://svn.apache.org/repos/asf/subversion
    Revision: 36366

    trunk/**
    branches/1.5.x/**
    branches/1.6.x/**
    README
    branches/1.4.x/STATUS
    branches/1.4.x/subversion/tests/cmdline/~

You may wish to version your viewspec files.  If so, you can use this
script in conjunction with 'svn cat' to fetch, parse, and act on a
versioned viewspec file:

    $ svn cat http://svn.example.com/specs/dev-spec.txt |
         __SCRIPTNAME__ checkout - /path/to/target/directory
         """.replace("__SCRIPTNAME__", __SCRIPTNAME__)

    all_the_lines_should_be_the_same(expected, sys_out_lines)

    all_the_lines_should_be_the_same([], sys_err_lines)


def test_that_viewspec_does_checkout_with_no_revision_specified(capsys):

    spec = """Format: 1
Url: http://svn.apache.org/repos/asf/svn

trunk/**
branches/1.5.x/**
branches/1.6.x/**
README
branches/1.4.x/STATUS
branches/1.4.x/s/tests/cmdline/~
"""

    perform_viewspec(
      ["checkout", (make_temp_file_containing(spec)), "a/b"])
    sys_out_lines, sys_err_lines = capsys.readouterr()

    expected = \
      """svn checkout "http://svn.apache.org/repos/asf/svn" "a/b" --depth=empty
      svn update "a/b/README" --set-depth=empty
      svn update "a/b/branches" --set-depth=empty
      svn update "a/b/branches/1.4.x" --set-depth=empty
      svn update "a/b/branches/1.4.x/STATUS" --set-depth=empty
      svn update "a/b/branches/1.4.x/s" --set-depth=empty
      svn update "a/b/branches/1.4.x/s/tests" --set-depth=empty
      svn update "a/b/branches/1.4.x/s/tests/cmdline" --set-depth=files
      svn update "a/b/branches/1.5.x" --set-depth=infinity
      svn update "a/b/branches/1.6.x" --set-depth=infinity
      svn update "a/b/trunk" --set-depth=infinity  """

    all_the_lines_should_be_the_same(expected, os_system_lines)
    all_the_lines_should_be_the_same([], sys_err_lines)
    all_the_lines_should_be_the_same([], sys_out_lines)


def all_the_lines_should_be_the_same(expected_lines,
                                     actual_lines):
    if not isinstance(expected_lines, list):
        expected_lines = expected_lines.splitlines()
    if not isinstance(actual_lines, list):
        actual_lines = actual_lines.splitlines()
    assert len(actual_lines) == len(expected_lines)
    for i, line in enumerate(actual_lines):
        assert line.strip() == expected_lines[i].strip()


def make_temp_file_containing(spec):
    new_file, filename = tempfile.mkstemp()
    os.write(new_file, spec)
    os.close(new_file)
    return filename
