blob: a152f064eef1d76d3feb64cd4a74b63126187515 [file]
#!/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()