#!/usr/bin/env python3

# Copyright 2020 The Bazel Authors. All rights reserved.
#
# 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 subprocess
import csv
import io
import os
import rpm_util
from rules_python.python.runfiles import runfiles

# This provides some tests for built RPMs, mostly by taking the built RPM and
# running rpm queries on it.
#
# Useful reading:
#
# - RPM queryformat documentation (shortish):
#   https://rpm.org/user_doc/query_format.html
#
# - In-depth RPM query documentation:
#   http://ftp.rpm.org/max-rpm/s1-rpm-query-parts.html
#
# - Specifically, about the --qf/--queryformat syntax:
#   http://ftp.rpm.org/max-rpm/s1-rpm-query-parts.html#S3-RPM-QUERY-QUERYFORMAT-OPTION
#
# - --queryformat tags list: http://ftp.rpm.org/max-rpm/ch-queryformat-tags.html
#
class PkgRpmBasicTest(unittest.TestCase):
    def setUp(self):
        self.runfiles = runfiles.Create()
        self.test_rpm_path = self.runfiles.Rlocation(
            "rules_pkg/tests/rpm/test_rpm.rpm")
        self.test_rpm_direct_path = self.runfiles.Rlocation(
            "rules_pkg/tests/rpm/test_rpm_direct.rpm")
        self.test_rpm_bzip2_path = self.runfiles.Rlocation(
            "rules_pkg/tests/rpm/test_rpm-bzip2.rpm")
        self.maxDiff = None

    def test_scriptlet_content(self):
        expected = b"""\
preinstall scriptlet (using /bin/sh):
echo pre
postinstall scriptlet (using /bin/sh):
echo post
preuninstall scriptlet (using /bin/sh):
echo preun
postuninstall scriptlet (using /bin/sh):
echo postun
"""

        output = subprocess.check_output(
            ["rpm", "-qp", "--scripts", self.test_rpm_path])

        self.assertEqual(output, expected)

    def test_basic_headers(self):
        fields = {
            "NAME": b"test_rpm",
            "VERSION": b"1.1.1",
            "RELEASE": b"2222",
            "ARCH": b"noarch",
            "GROUP": b"Unspecified",
            "SUMMARY": b"pkg_rpm test rpm summary",
        }
        for fieldname, expected in fields.items():
            output = subprocess.check_output([
                "rpm", "-qp", "--queryformat", "%{" + fieldname + "}",
                self.test_rpm_path
            ])

            self.assertEqual(
                output, expected,
                "RPM Tag {} does not match expected value".format(fieldname))

    def test_contents(self):
        manifest_file = self.runfiles.Rlocation(
            "rules_pkg/tests/rpm/manifest.csv")
        manifest_specs = {}
        with open(manifest_file, "r", newline='', encoding="utf-8") as fh:
            manifest_reader = csv.DictReader(fh)
            manifest_specs = {r['path']: r for r in manifest_reader}

        rpm_specs = rpm_util.read_rpm_filedata(self.test_rpm_path)

        self.assertDictEqual(manifest_specs, rpm_specs)
        # Transitively, this one should work too -- doesn't use pkg_filegroup
        # directly.
        rpm_direct_specs = rpm_util.read_rpm_filedata(self.test_rpm_direct_path)
        self.assertDictEqual(rpm_specs, rpm_direct_specs)

    def test_preamble_metadata(self):
        metadata_prefix = "rules_pkg/tests/rpm"

        rpm_filename = os.path.basename(self.test_rpm_path)
        rpm_basename = os.path.splitext(rpm_filename)[0]

        # Tuples of:
        # Metadata name, RPM Tag prefix, exclusion list (currently only support "startswith")
        #
        # The exclusions should probably be regexps at some point, but right
        # now, our job is relatively easy.  They only operate on the
        # "capability" portion of the tag.
        test_md = [
            ("conflicts", "CONFLICT", []),
            # rpm packages implicitly provide themselves, something like:
            # "test_rpm = 1.1.1-2222".  We don't bother testing this, since we
            # don't take direct action to specify it.
            ("provides",  "PROVIDE",  [rpm_basename]),
            # Skip rpmlib-related requirements; they are often dependent on the
            # version of `rpm` we are using.
            ("requires",  "REQUIRE",  ["rpmlib"]),
        ]
        for (mdtype, tag, exclusions) in test_md:
            md_file = self.runfiles.Rlocation(
                os.path.join(metadata_prefix, mdtype + ".csv"))

            with open(md_file, "r", newline='', encoding="utf-8") as fh:
                md_reader = csv.DictReader(fh, delimiter=':')
                # I heard you like functional programming ;)
                #
                # This produces a list of outputs whenever the "capability"
                # attribute starts with any of the values in the "exclusions"
                # list.
                md_specs_unsorted = [line for line in md_reader
                                     if not any(line['capability'].startswith(e)
                                                for e in exclusions)]
                # And this sorts it, ordering by the sorted "association list"
                # form of the dictionary.
                #
                # The sorting of the key values is not necessary with versions
                # of python3 (3.5+, I believe) that have dicts maintain
                # insertion order.
                md_specs = sorted(md_specs_unsorted,
                                  key = lambda x: sorted(x.items()))


            # This typically becomes the equivalent of:
            #
            #   '[%{PROVIDENEVRS};%{PROVIDEFLAGS:deptype}\n]'
            #
            # as passed to `rpm --queryformat`
            rpm_queryformat = (
                # NEVRS = Name Epoch Version Release (plural), which look something like:
                #   rpmlib(CompressedFileNames) <= 3.0.4-1
                # or:
                #   bash
                "[%{{{tag}NEVRS}}"
                # Flags associated with the dependency type.  This used to
                # evaluate in what "sense" the dependency was added.
                #
                # Values often include things like:
                #
                # - "interp" for scriptlet interpreter dependencies
                # - "postun" for dependencies of the "postun" scriptlet
                # - "manual" for values that are explicitly specified
                ":%{{{tag}FLAGS:deptype}}"
                "\n]"
            ).format(tag = tag)

            rpm_queryformat_fieldnames = [
                "capability",
                "sense",
            ]

            rpm_output = subprocess.check_output(
                ["rpm", "-qp", "--queryformat", rpm_queryformat, self.test_rpm_path])

            sio = io.StringIO(rpm_output.decode('utf-8'))
            rpm_output_reader = csv.DictReader(
                sio, delimiter=':', fieldnames=rpm_queryformat_fieldnames)

            # Get everything in the same order as the read-in metadata file
            rpm_outputs_filtered_unsorted = [line for line in rpm_output_reader
                                             if not any(line['capability'].startswith(e)
                                                        for e in exclusions)]

            rpm_outputs_filtered = sorted(rpm_outputs_filtered_unsorted, key = lambda x: sorted(x.items()))

            for expected, actual in zip(md_specs, rpm_outputs_filtered):
                self.assertDictEqual(expected, actual,
                                     msg="{} metadata discrepancy".format(mdtype))

    def test_compression_none_provided(self):
        # Test when we don't provide "binary_payload_compression" to pkg_rpm
        my_rpm = self.test_rpm_path
        rpm_output = subprocess.check_output(
            ["rpm", "-qp", "--queryformat", "%{PAYLOADCOMPRESSOR}", my_rpm])
        sio = io.StringIO(rpm_output.decode('utf-8'))
        actual_compressor = sio.read()
        # `bzip2` compression was, AFAICT, never the default for rpmbuild(8),
        # and never will be, so this should be fine.
        self.assertNotEqual(actual_compressor, 'bzip2')

    def test_compression_passthrough(self):
        # Test when we provide "binary_payload_compression" to pkg_rpm
        my_rpm = self.test_rpm_bzip2_path
        rpm_output = subprocess.check_output(
            ["rpm", "-qp", "--queryformat", "%{PAYLOADCOMPRESSOR}", my_rpm])
        sio = io.StringIO(rpm_output.decode('utf-8'))
        actual_compressor = sio.read()
        self.assertEqual(actual_compressor, 'bzip2')

    def test_mtimes(self):
        # Modification times should be something close to when the package was
        # built.  Since we do not know when the package was actually built, we
        # can just check for something that is non-zero.
        #
        # See also #486.

        filedata = rpm_util.read_rpm_filedata(
            self.test_rpm_path,
            query_tag_map={
                "FILENAMES": "path",
                "FILEMTIMES": "mtime",
            }
        )

        self.assertNotEqual(
            len(filedata),
            0,
            "rpm_util.read_rpm_filedata() produced no output"
        )

        filedata_shortened = {
            path: int(data["mtime"])
            for path, data in filedata.items()
        }

        files_with_zero_mtimes = dict(filter(
            lambda kv: kv[1] == 0,
            filedata_shortened.items(),
        ))

        self.assertDictEqual(
            files_with_zero_mtimes,
            {},
            "No files should have zero mtimes without source_date_epoch or source_date_epoch_file")


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