Repository: vmware/tern
Pull request #558 (2020-02-27 21:48:20)

classes: Added FileData object list to ImageLayer
We add a list of FileData objects representing a list of files
found in a container image layer. The property, and methods closely
follow the ones for the Package list. We also take into account
the addition of the list of files in the dictionary representation
with and without templates. We modify the test and test fixtures
accordingly.

Signed-off-by: Nisha K <nishak@vmware.com>


Patch:
diff --git a/tern/classes/image_layer.py b/tern/classes/image_layer.py
index 40e9731..9689c4d 100644
--- a/tern/classes/image_layer.py
+++ b/tern/classes/image_layer.py
@@ -1,10 +1,11 @@
 # -*- coding: utf-8 -*-
 #
-# Copyright (c) 2017-2019 VMware, Inc. All Rights Reserved.
+# Copyright (c) 2017-2020 VMware, Inc. All Rights Reserved.
 # SPDX-License-Identifier: BSD-2-Clause
 
 
 from tern.classes.package import Package
+from tern.classes.file_data import FileData
 from tern.classes.origins import Origins
 from tern.utils import rootfs
 from tern.utils.general import prop_names
@@ -30,18 +31,24 @@ class ImageLayer:
         created this layer by importing it from another image
         files_analyzed: whether the files in this layer are analyzed or not
         analyzed_output: the result of the file analysis
+        files: a list of files included in the image layer
     methods:
         add_package: adds a package to the layer
         remove_package: removes a package from the layer
         to_dict: returns a dict representation of the instance
         get_package_names: returns a list of package names
-        gen_fs_hash: calculate the filesystem hash'''
+        gen_fs_hash: calculate the filesystem hash
+        add_file: adds a file to the layer
+        remove_file: given the file path, remove a file object from the
+        list of files in this layer
+        get_file_paths: Get a list of file paths in the image layer'''
     def __init__(self, diff_id, tar_file=None, created_by=None):
         self.__diff_id = diff_id
         self.__fs_hash = ''
         self.__tar_file = tar_file
         self.__created_by = created_by
         self.__packages = []
+        self.__files = []
         self.__origins = Origins()
         self.__import_image = None
         self.__import_str = ''
@@ -58,6 +65,10 @@ class ImageLayer:
     def packages(self):
         return self.__packages
 
+    @property
+    def files(self):
+        return self.__files
+
     @property
     def fs_hash(self):
         return self.__fs_hash
@@ -152,11 +163,42 @@ class ImageLayer:
             self.__packages.remove(self.__packages[rem_index])
         return success
 
+    def add_file(self, filedata):
+        if isinstance(filedata, FileData):
+            if filedata.path not in self.get_file_paths():
+                self.__files.append(filedata)
+        else:
+            raise TypeError('Object type is {0}, should be FileData'.format(
+                type(filedata)))
+
+    def get_file_paths(self):
+        '''Get the list of file paths in this layer'''
+        file_path_list = []
+        for f in self.files:
+            file_path_list.append(f.path)
+        return file_path_list
+
+    def remove_file(self, file_path):
+        '''Given the file path, remove the file object from the list of files
+        in the layer'''
+        rem_index = 0
+        success = False
+        for index in range(0, len(self.__files)):
+            if self.__files[index].path == file_path:
+                rem_index = index
+                success = True
+                break
+        if success:
+            self.__files.remove(self.__files[rem_index])
+        return success
+
     def to_dict(self, template=None):
         '''Return a dictionary representation of the image layer object'''
         layer_dict = {}
         # for packages call each package object's to_dict method
         pkg_list = [pkg.to_dict(template) for pkg in self.packages]
+        # for files call each FileData object's to_dict method
+        file_list = [f.to_dict(template) for f in self.files]
         if template:
             # use the template mapping for key names
             for key, prop in prop_names(self):
@@ -172,12 +214,18 @@ class ImageLayer:
             if 'packages' in template.image_layer().keys():
                 layer_dict.update(
                     {template.image_layer()['packages']: pkg_list})
+            # update the 'files' if it exists in the mapping
+            if 'files' in template.image_layer().keys():
+                layer_dict.update(
+                    {template.image_layer()['files']: file_list})
         else:
             # directly use property names
             for key, prop in prop_names(self):
                 layer_dict.update({prop: self.__dict__[key]})
             # update with 'packages' info
             layer_dict.update({'packages': pkg_list})
+            # update with 'files' info
+            layer_dict.update({'files': file_list})
             # take care of the 'origins' property
             layer_dict.update({'origins': self.origins.to_dict()})
         return layer_dict


Test patch:
diff --git a/tests/test_class_image_layer.py b/tests/test_class_image_layer.py
index 8946259..40b172a 100644
--- a/tests/test_class_image_layer.py
+++ b/tests/test_class_image_layer.py
@@ -1,12 +1,13 @@
 # -*- coding: utf-8 -*-
 #
-# Copyright (c) 2017-2019 VMware, Inc. All Rights Reserved.
+# Copyright (c) 2017-2020 VMware, Inc. All Rights Reserved.
 # SPDX-License-Identifier: BSD-2-Clause
 
 import unittest
 
 from tern.classes.image_layer import ImageLayer
 from tern.classes.package import Package
+from tern.classes.file_data import FileData
 from test_fixtures import TestTemplate1
 from test_fixtures import TestTemplate2
 
@@ -61,13 +62,33 @@ class TestClassImageLayer(unittest.TestCase):
         self.assertTrue(self.layer.remove_package('y'))
         self.assertFalse(self.layer.remove_package('y'))
 
+    def testAddFile(self):
+        err = "Object type String, should be FileData"
+        file1 = FileData('file1', 'path/to/file1')
+        self.layer.add_file(file1)
+        self.assertEqual(len(self.layer.files), 1)
+        with self.assertRaises(TypeError, msg=err):
+            self.layer.add_file("afile")
+
+    def testRemoveFile(self):
+        file1 = FileData('file1', 'path/to/file1')
+        self.layer.add_file(file1)
+        self.assertFalse(self.layer.remove_file('file1'))
+        self.assertTrue(self.layer.remove_file('path/to/file1'))
+        self.assertFalse(self.layer.remove_file('path/to/file1'))
+
     def testToDict(self):
         p1 = Package('x')
+        f1 = FileData('file1', 'path/to/file1')
         self.layer.add_package(p1)
+        self.layer.add_file(f1)
         a_dict = self.layer.to_dict()
         self.assertEqual(a_dict['diff_id'], '123abc')
         self.assertEqual(len(a_dict['packages']), 1)
         self.assertEqual(a_dict['packages'][0]['name'], 'x')
+        self.assertEqual(len(a_dict['files']), 1)
+        self.assertEqual(a_dict['files'][0]['name'], 'file1')
+        self.assertEqual(a_dict['files'][0]['path'], 'path/to/file1')
         self.assertEqual(a_dict['tar_file'], 'path/to/tar')
 
     def testToDictTemplate(self):
@@ -75,15 +96,19 @@ class TestClassImageLayer(unittest.TestCase):
         template2 = TestTemplate2()
         p1 = Package('x')
         self.layer.add_package(p1)
+        f1 = FileData('file1', 'path/to/file1')
+        self.layer.add_file(f1)
         dict1 = self.layer.to_dict(template1)
         dict2 = self.layer.to_dict(template2)
-        self.assertEqual(len(dict1.keys()), 3)
+        self.assertEqual(len(dict1.keys()), 4)
         self.assertEqual(dict1['layer.diff'], '123abc')
         self.assertEqual(dict1['layer.tarfile'], 'path/to/tar')
         self.assertEqual(len(dict1['layer.packages']), 1)
-        self.assertEqual(len(dict2.keys()), 4)
+        self.assertEqual(len(dict1['layer.files']), 1)
+        self.assertEqual(len(dict2.keys()), 5)
         self.assertFalse(dict2['notes'])
         self.assertFalse(dict2['layer.packages'][0]['notes'])
+        self.assertFalse(dict2['layer.files'][0]['notes'])
 
     def testGetPackageNames(self):
         p1 = Package('x')
@@ -91,6 +116,14 @@ class TestClassImageLayer(unittest.TestCase):
         pkgs = self.layer.get_package_names()
         self.assertEqual(pkgs[0], 'x')
 
+    def testGetFilePaths(self):
+        f1 = FileData('file1', 'path/to/file1')
+        f2 = FileData('file2', 'path/to/file2')
+        self.layer.add_file(f1)
+        self.layer.add_file(f2)
+        file_paths = self.layer.get_file_paths()
+        self.assertEqual(file_paths, ['path/to/file1', 'path/to/file2'])
+
 
 if __name__ == '__main__':
     unittest.main()
diff --git a/tests/test_fixtures.py b/tests/test_fixtures.py
index e9facf7..e43bd78 100644
--- a/tests/test_fixtures.py
+++ b/tests/test_fixtures.py
@@ -41,7 +41,8 @@ class TestTemplate1(Template):
     def image_layer(self):
         return {'diff_id': 'layer.diff',
                 'tar_file': 'layer.tarfile',
-                'packages': 'layer.packages'}
+                'packages': 'layer.packages',
+                'files': 'layer.files'}
 
     def image(self):
         return {'image_id': 'image.id',
@@ -70,7 +71,8 @@ class TestTemplate2(Template):
     def image_layer(self):
         mapping = {'diff_id': 'layer.diff',
                    'tar_file': 'layer.tarfile',
-                   'packages': 'layer.packages'}
+                   'packages': 'layer.packages',
+                   'files': 'layer.files'}
         # we update the mapping with another defined mapping
         mapping.update(self.origins())
         return mapping