blob: 8a4e24910d358ed0bc665d056ea2ece2f35f4e99 [file] [log] [blame]
# 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.
#
# -*- coding: utf-8 -*-
"""Testing for archive."""
import codecs
from io import BytesIO
import os
import sys
import tarfile
import unittest
from python.runfiles import runfiles
from pkg.private import archive
class DebInspect(object):
"""Class to open and unpack a .deb file so we can examine it."""
def __init__(self, deb_file):
self.deb_version = None
self.data = None
self.control = None
with archive.SimpleArReader(deb_file) as f:
info = f.next()
while info:
if info.filename == 'debian-binary':
self.deb_version = info.data
elif info.filename == 'control.tar.gz':
self.control = info.data
elif info.filename == 'data.tar.gz':
self.data = info.data
else:
raise Exception('Unexpected file: %s' % info.filename)
info = f.next()
def get_deb_ctl_file(self, file_name):
"""Extract a control file."""
with tarfile.open(mode='r:gz', fileobj=BytesIO(self.control)) as f:
for info in f:
if info.name == './' + file_name:
return codecs.decode(f.extractfile(info).read(), 'utf-8')
raise Exception('Could not find control file: %s' % file_name)
class PkgDebTest(unittest.TestCase):
"""Testing for pkg_deb rule."""
def setUp(self):
super(PkgDebTest, self).setUp()
self.runfiles = runfiles.Create()
# Note: Rlocation requires forward slashes. os.path.join() will not work.
self.deb_path = self.runfiles.Rlocation('rules_pkg/tests/deb/fizzbuzz_4.5.6_all.deb')
self.deb_file = DebInspect(self.deb_path)
def assert_control_content(self, expected, match_order=False):
self.assert_tar_stream_content(
BytesIO(self.deb_file.control),
expected,
match_order=match_order)
def assert_data_content(self, expected, match_order=True):
self.assert_tar_stream_content(
BytesIO(self.deb_file.data),
expected,
match_order=match_order)
def assert_tar_stream_content(self, data, expected, match_order=True):
"""Assert that tarfile contains exactly the entry described by `expected`.
Args:
data: the tar file stream to check.
expected: an array describing the expected content of the TAR file.
Each entry in that list should be a dictionary where each field
is a field to test in the corresponding TarInfo. For
testing the presence of a file "x", then the entry could simply
be `{'name': 'x'}`, the missing field will be ignored. To match
the content of a file entry, use the key 'data'.
match_order: True if files must match in order as well as properties.
"""
expected_by_name = {}
for e in expected:
expected_by_name[e['name']] = e
with tarfile.open(mode='r:*', fileobj=data) as f:
i = 0
for info in f:
error_msg = 'Extraneous file at end of archive: %s' % (
info.name
)
self.assertLess(i, len(expected), error_msg)
name_in_tar_file = getattr(info, 'name')
if match_order:
want = expected[i]
else:
want = expected_by_name[name_in_tar_file]
for k, v in want.items():
if k == 'data':
value = f.extractfile(info).read()
elif k == 'name':
# The test data uses / as path sep, but the tarball is in OS native
# format. This aligns the tarball name back to what we expect.
value = name_in_tar_file.replace(os.path.sep, '/')
elif k == 'isdir':
value = info.isdir()
elif k == "name" and sys.platform == 'win32':
value = getattr(current, k).replace("\\", "/")
else:
value = getattr(info, k)
error_msg = ' '.join([
'Value `%s` for key `%s` of file' % (value, k),
'%s in archive does' % info.name,
'not match expected value `%s`' % v
])
self.assertEqual(value, v, error_msg)
i += 1
if i < len(expected):
self.fail('Missing file %s' % expected[i])
def test_expected_files(self):
# Check the set of 'test-tar-basic-*' smoke test.
expected = [
{'name': 'etc', 'isdir': True,
'uid': 24, 'gid': 42, 'uname': 'foobar', 'gname': 'fizzbuzz'},
{'name': 'etc/nsswitch.conf',
'mode': 0o644,
'uid': 24, 'gid': 42, 'uname': 'foobar', 'gname': 'fizzbuzz'
},
{'name': 'usr', 'isdir': True,
'uid': 42, 'gid': 24, 'uname': 'fizzbuzz', 'gname': 'foobar'},
{'name': 'usr/bin', 'isdir': True},
{'name': 'usr/bin/java', 'linkname': '/path/to/bin/java'},
{'name': 'usr/fizzbuzz',
'mode': 0o755,
'uid': 42, 'gid': 24, 'uname': 'fizzbuzz', 'gname': 'foobar'},
]
self.assert_data_content(expected)
def test_description(self):
control = self.deb_file.get_deb_ctl_file('control')
fields = [
'Package: fizzbuzz',
'Depends: dep1, dep2',
'Built-Using: some_test_data',
'Replaces: oldpkg',
'Breaks: oldbrokenpkg',
'Provides: hello',
'License: Apache-2.0',
]
# TODO(https://github.com/bazelbuild/rules_pkg/issues/214): This can not
# pass on Windows Until we rewrite how description is passed
if sys.platform != 'win32':
fields.extend([
'Description: toto ®, Й, ק ,م, ๗, あ, 叶, 葉, 말, ü and é\n more\n',
'Maintainer: soméone@somewhere.com',
])
for field in fields:
if control.find(field) < 0:
self.fail('Missing control field: <%s> in <%s>' % (field, control))
def test_control_files(self):
expected = [
{'name': './conffiles', 'mode': 0o644},
{'name': './config', 'mode': 0o755},
{'name': './control', 'mode': 0o644},
{'name': './preinst', 'mode': 0o755},
{'name': './md5sums', 'mode': 0o644},
{'name': './templates', 'mode': 0o644},
{'name': './triggers', 'mode': 0o644},
]
self.assert_control_content(expected, match_order=False)
def test_conffiles(self):
conffiles = self.deb_file.get_deb_ctl_file('conffiles')
self.assertEqual(
conffiles,
'/etc/nsswitch.conf\n/etc/other\n')
def test_config(self):
config = self.deb_file.get_deb_ctl_file('config')
self.assertEqual(config, '# test config file\n')
def test_preinst(self):
preinst = self.deb_file.get_deb_ctl_file('preinst')
self.assertEqual(
preinst,
'#!/usr/bin/env bash\n'
'# tete ®, Й, ק ,م, ๗, あ, 叶, 葉, 말, ü and é\n'
'echo fnord\n')
def test_templates(self):
templates = self.deb_file.get_deb_ctl_file('templates')
for field in ('Template: deb/test', 'Type: string'):
if templates.find(field) < 0:
self.fail('Missing template field: <%s> in <%s>' % (field, templates))
def test_triggers(self):
triggers = self.deb_file.get_deb_ctl_file('triggers')
self.assertEqual(
triggers,
'# tutu ®, Й, ק ,م, ๗, あ, 叶, 葉, 말, ü and é\n'
'some-trigger\n')
def test_changes(self):
changes_path = self.runfiles.Rlocation(
'rules_pkg/tests/deb/fizzbuzz_4.5.6_all.changes')
with open(changes_path, 'r', encoding='utf-8') as f:
content = f.read()
for field in (
'Urgency: low',
'Distribution: trusty'):
if content.find(field) < 0:
self.fail('Missing template field: <%s> in <%s>' % (field, content))
# TODO(https://github.com/bazelbuild/rules_pkg/issues/214): This can not
# pass on Windows until we rewrite how description is passed.
if sys.platform == 'win32':
return
# From the spec:
# In a .changes file, the Description field contains a summary of the
# descriptions for the packages being uploaded. For this case, the first
# line of the field value (the part on the same line as Description:) is
# always empty. It is a multiline field, with one line per package. Each
# line is indented by one space and contains the name of a binary package,
# a space, a hyphen (-), a space, and the short description line from that
# package.
d_expect = 'Description:\n fizzbuzz - toto ®, Й, ק ,م, ๗, あ, 叶, 葉, 말, ü and é\n'
d_start = content.find(d_expect)
if d_start < 0:
self.fail(
'Could not find expected description (%s) in\n=====%s=====' %
(d_expect, content))
# Check that the next line is the start of a new description, rather than
# a continuation.
self.assertTrue(
content[d_start + len(d_expect)].isupper(),
'Description has unexpected characters at end (%s)' % content)
self.maxDiff = None
expect = '''Format: 1\.8
Date: Thu Jan 1 \d{2}:00:00 1970
Source: fizzbuzz
Binary: fizzbuzz
Architecture: all
Version: 4\.5\.6
Distribution: trusty
Urgency: low
Maintainer: soméone@somewhere.com
Changed-By: soméone@somewhere.com
Description:
fizzbuzz - toto ®, Й, ק ,م, ๗, あ, 叶, 葉, 말, ü and é
Changes:
fizzbuzz \(4\.5\.6\) trusty; urgency=low
Changes are tracked in revision control\.
Files:
[a-f0-9]{32} \d{4} contrib/devel optional fizzbuzz_4\.5\.6_all\.deb
Checksums-Sha1:
[a-f0-9]{40} \d{4} fizzbuzz_4\.5\.6_all\.deb
Checksums-Sha256:
[a-f0-9]{64} \d{4} fizzbuzz_4\.5\.6_all\.deb
'''
self.assertRegex(content, expect)
if __name__ == '__main__':
unittest.main()