4260 pkg info should print the classification
authorBrock Pytlik <bpytlik@sun.com>
Sat, 08 Nov 2008 11:08:13 -0800
changeset 688 32a3ca40676e
parent 687 3d7060f40501
child 689 1cb7154e1592
4260 pkg info should print the classification
doc/api_versions.txt
doc/client_api_versions.txt
src/client.py
src/modules/actions/attribute.py
src/modules/client/api.py
src/tests/cli/t_api_info.py
src/tests/cli/t_pkg_info.py
--- a/doc/api_versions.txt	Fri Nov 07 16:55:36 2008 -0800
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,196 +0,0 @@
-Version 5:
-Compatible with clients using versions 1-4 as long as they have a generic
-APIException. This is the case for PackageManager and UpdateManaget.
-Changes:
-plan_install and plan_update_all can now raise PermissionsException.
-
-Version 4:
-Compatible with clients using versions 1, 2, and 3
-Changes:
-Modifies where certain progress tracking calls were made, calling 
-    evaluate_start much sooner.
-Adds refresh tracking to progress.py. This allows for active feedback when
-    the catalogs of authorities are being refreshed.
-
-Version 3:
-Compatible with clients using Versions 1 and 2
-Changes:
-Adds an optional argument to info which determines whether detailed information
-    about actions will be returned
-Adds the following new fields to PackageInfo objects: links, hardlinks,
-    files, dirs, dependencies
-
-Version 2:
-Compatible with clients using Version 1
-Changes:
-Adds the optional argument update_index to plan_install, plan_uninstall, and 
-    plan_update_all. When the argument is false, no automatic update to the
-    index occurs. By default, the argument is true.
-
-Version 1:
-Incompatible with clients using Version 0
-Changes:
-plan_install now returns a tuple of whether there is anything to do and 
-    a catalog refresh exception, if one was caught. In this, it mirrors the
-    first and third return values from plan_update_all.
-
-Version 0:
-        def __init__(self, img_path, version_id, progesstracker,
-            cancel_state_callable, pkg_client_name):
-                """Constructs an ImageInterface. img_path should point to an
-                existing image. version_id indicates the version of the api
-                the client is expecting to use. progesstracker is the
-                progresstracker the client wants the api to use for UI
-                callbacks. cancel_state_callable is a function which the client
-                wishes to have called each time whether the operation can be
-                canceled changes. It can raise VersionException and
-                ImageNotFoundException."""
-
-        def plan_install(self, pkg_list, filters, refresh_catalogs=True,
-            noexecute=False, verbose=False):
-                """Contructs a plan to install the packages provided in
-                pkg_list. pkg_list is a list of packages to install. filters
-                is a list of filters to apply to the actions of the installed
-                packages. refresh_catalogs controls whether the catalogs will
-                automatically be refreshed. noexecute determines whether the
-                history will be recorded after planning is finished. verbose
-                controls whether verbose debugging output will be printed to the
-                terminal. Its existence is temporary. If there are things to do
-                to complete the install, it returns True, otherwise it returns
-                False. It can raise InvalidCertException, PlanCreationException,
-                NetworkUnavailableException, and InventoryException. The
-                noexecute argument is included for compatibility with
-                operational history. The hope is it can be removed in the
-                future."""
-
-        def plan_uninstall(self, pkg_list, recursive_removal, noexecute=False,
-            verbose=False):
-                """Contructs a plan to uninstall the packages provided in
-                pkg_list. pkg_list is a list of packages to install.
-                recursive_removal controls whether recursive removal is
-                allowed. noexecute determines whether the history will be
-                recorded after planning is finished. verbose controls whether
-                verbose debugging output will be printed to the terminal. Its
-                existence is temporary. If there are things to do to complete
-                the uninstall, it returns True, otherwise it returns False. It
-                can raise NonLeafPackageException and PlanCreationException."""
-
-        def plan_update_all(self, actual_cmd, refresh_catalogs=True,
-            noexecute=False, force=False, pkgs_must_be_up_to_date=None,
-            verbose=False):
-                """Creates a plan to update all packages on the system to the
-                latest known versions. actual_cmd is the command used to start
-                the client. It is used to determine the image to check whether
-                SUNWipkg is up to date. refresh_catalogs controls whether the
-                catalogs will automatically be refreshed. noexecute determines
-                whether the history will be recorded after planning is finished.
-                force controls whether update should proceed even if ipkg is not
-                up to date. verbose controls whether verbose debugging output
-                will be printed to the terminal. Its existence is temporary. It
-                returns a tuple of three things. The first is a boolean which
-                tells the client whether there is anything to do. The second
-                tells whether the image is an opensolaris image. The third is
-                either None, or an exception which indicates partial success.
-                This is currently used to indicate a failure in refreshing
-                catalogs. It can raise CatalogRefreshException,
-                IpkgOutOfDateException, NetworkUnavailableException, and
-                PlanCreationException."""
-
-        def describe(self):
-                """Returns None if no plan is ready yet, otherwise returns
-                a PlanDescription"""
-
-        def prepare(self):
-                """Takes care of things which must be done before the plan
-                can be executed. This includes downloading the packages to
-                disk and preparing the indexes to be updated during
-                execution. It can raise ProblematicPermissionsIndexException,
-                and PlanMissingException. Should only be called once a
-                plan_X method has been called."""
-
-        def execute_plan(self, be_name=None):
-                """Executes the plan. This is uncancelable one it begins. It
-                can raise  CorruptedIndexException,
-                ProblematicPermissionsIndexException, ImageplanStateException,
-                ImageUpdateOnLiveImageException, and PlanMissingException.
-                Should only be called after the prepare method has been
-                called."""
-
-        def refresh(self, full_refresh, auths=None):
-                """Refreshes the catalogs. full_refresh controls whether to do
-                a full retrieval of the catalog from the authority or only
-                update the existing catalog. auths is a list of authorities to
-                refresh. Passing an empty list or using the default value means
-                all known authorities will be refreshed. While it currently
-                returns an image object, this is an expedient for allowing
-                existing code to work while the rest of the API is put into
-                place."""
-
-        def info(self, fmri_strings, local, get_licenses):
-                """Gathers information about fmris. fmri_strings is a list
-                of fmri_names for which information is desired. local
-                determines whether to retrieve the information locally.
-                get_licenses determines whether to retrieve the text of
-                the licenses. It returns a dictionary of lists. The keys
-                for the dictionary are the constants specified in the class
-                definition. The values are lists of PackageInfo objects or
-                strings."""
-
-        def can_be_canceled(self):
-                """Returns true if the API is in a cancelable state."""
-
-        def reset(self):
-                """Resets the API back the the initial state. Note:
-                this does not necessarily return the disk to its initial state
-                since the indexes or download cache may have been changed by
-                the prepare method."""
-
-        def cancel(self):
-                """Used for asynchronous cancelation. It returns the API
-                to the state it was in prior to the current method being
-                invoked.  Canceling during a plan phase returns the API to
-                its initial state. Canceling during prepare puts the API
-                into the state it was in just after planning had completed.
-                Plan execution cannot be canceled. A call to this method blocks
-                until the canelation has happened. Note: this does not
-                necessarily return the disk to its initial state since the
-                indexes or download cache may have been changed by the
-                prepare method."""
-
-class PlanDescription(object):
-        """A class which describes the changes the plan will make. It
-        provides a list of tuples of PackageInfo's. The first item in the
-        tuple is the package that is being changed. The second item in the
-        tuple is the package that will be in the image after the change."""
-
-        def get_changes(self):
-
-class LicenseInfo(object):
-        """A class representing the license information a package
-        provides."""
-
-        def get_text(self):
-
-class PackageInfo(object):
-        """A class capturing the information about packages that a client
-        could need. The fmri is guaranteed to be set. All other values may
-        be None, depending on how the PackageInfo instance was created."""
-
-        # Possible package installation states
-        INSTALLED = 1
-        NOT_INSTALLED = 2
-
-        self.pkg_stem
-        self.summary
-        self.state
-        self.authority
-        self.preferred_authority
-        self.version
-        self.build_release
-        self.branch
-        self.packaging_date
-        self.size
-        self.fmri
-        self.licenses
-
-        def __str__(self):
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/client_api_versions.txt	Sat Nov 08 11:08:13 2008 -0800
@@ -0,0 +1,203 @@
+Version 6:
+Compatible with clients using versions 1-5
+Changes:
+Adds a new field to PackageInfo, category_info_list, which is a list of 
+     PackageCategory objects. These objects contain the scheme and category
+     information for packages.
+
+Version 5:
+Compatible with clients using versions 1-4 as long as they have a generic
+APIException. This is the case for PackageManager and UpdateManaget.
+Changes:
+plan_install and plan_update_all can now raise PermissionsException.
+
+Version 4:
+Compatible with clients using versions 1, 2, and 3
+Changes:
+Modifies where certain progress tracking calls were made, calling 
+    evaluate_start much sooner.
+Adds refresh tracking to progress.py. This allows for active feedback when
+    the catalogs of authorities are being refreshed.
+
+Version 3:
+Compatible with clients using Versions 1 and 2
+Changes:
+Adds an optional argument to info which determines whether detailed information
+    about actions will be returned
+Adds the following new fields to PackageInfo objects: links, hardlinks,
+    files, dirs, dependencies
+
+Version 2:
+Compatible with clients using Version 1
+Changes:
+Adds the optional argument update_index to plan_install, plan_uninstall, and 
+    plan_update_all. When the argument is false, no automatic update to the
+    index occurs. By default, the argument is true.
+
+Version 1:
+Incompatible with clients using Version 0
+Changes:
+plan_install now returns a tuple of whether there is anything to do and 
+    a catalog refresh exception, if one was caught. In this, it mirrors the
+    first and third return values from plan_update_all.
+
+Version 0:
+        def __init__(self, img_path, version_id, progesstracker,
+            cancel_state_callable, pkg_client_name):
+                """Constructs an ImageInterface. img_path should point to an
+                existing image. version_id indicates the version of the api
+                the client is expecting to use. progesstracker is the
+                progresstracker the client wants the api to use for UI
+                callbacks. cancel_state_callable is a function which the client
+                wishes to have called each time whether the operation can be
+                canceled changes. It can raise VersionException and
+                ImageNotFoundException."""
+
+        def plan_install(self, pkg_list, filters, refresh_catalogs=True,
+            noexecute=False, verbose=False):
+                """Contructs a plan to install the packages provided in
+                pkg_list. pkg_list is a list of packages to install. filters
+                is a list of filters to apply to the actions of the installed
+                packages. refresh_catalogs controls whether the catalogs will
+                automatically be refreshed. noexecute determines whether the
+                history will be recorded after planning is finished. verbose
+                controls whether verbose debugging output will be printed to the
+                terminal. Its existence is temporary. If there are things to do
+                to complete the install, it returns True, otherwise it returns
+                False. It can raise InvalidCertException, PlanCreationException,
+                NetworkUnavailableException, and InventoryException. The
+                noexecute argument is included for compatibility with
+                operational history. The hope is it can be removed in the
+                future."""
+
+        def plan_uninstall(self, pkg_list, recursive_removal, noexecute=False,
+            verbose=False):
+                """Contructs a plan to uninstall the packages provided in
+                pkg_list. pkg_list is a list of packages to install.
+                recursive_removal controls whether recursive removal is
+                allowed. noexecute determines whether the history will be
+                recorded after planning is finished. verbose controls whether
+                verbose debugging output will be printed to the terminal. Its
+                existence is temporary. If there are things to do to complete
+                the uninstall, it returns True, otherwise it returns False. It
+                can raise NonLeafPackageException and PlanCreationException."""
+
+        def plan_update_all(self, actual_cmd, refresh_catalogs=True,
+            noexecute=False, force=False, pkgs_must_be_up_to_date=None,
+            verbose=False):
+                """Creates a plan to update all packages on the system to the
+                latest known versions. actual_cmd is the command used to start
+                the client. It is used to determine the image to check whether
+                SUNWipkg is up to date. refresh_catalogs controls whether the
+                catalogs will automatically be refreshed. noexecute determines
+                whether the history will be recorded after planning is finished.
+                force controls whether update should proceed even if ipkg is not
+                up to date. verbose controls whether verbose debugging output
+                will be printed to the terminal. Its existence is temporary. It
+                returns a tuple of three things. The first is a boolean which
+                tells the client whether there is anything to do. The second
+                tells whether the image is an opensolaris image. The third is
+                either None, or an exception which indicates partial success.
+                This is currently used to indicate a failure in refreshing
+                catalogs. It can raise CatalogRefreshException,
+                IpkgOutOfDateException, NetworkUnavailableException, and
+                PlanCreationException."""
+
+        def describe(self):
+                """Returns None if no plan is ready yet, otherwise returns
+                a PlanDescription"""
+
+        def prepare(self):
+                """Takes care of things which must be done before the plan
+                can be executed. This includes downloading the packages to
+                disk and preparing the indexes to be updated during
+                execution. It can raise ProblematicPermissionsIndexException,
+                and PlanMissingException. Should only be called once a
+                plan_X method has been called."""
+
+        def execute_plan(self, be_name=None):
+                """Executes the plan. This is uncancelable one it begins. It
+                can raise  CorruptedIndexException,
+                ProblematicPermissionsIndexException, ImageplanStateException,
+                ImageUpdateOnLiveImageException, and PlanMissingException.
+                Should only be called after the prepare method has been
+                called."""
+
+        def refresh(self, full_refresh, auths=None):
+                """Refreshes the catalogs. full_refresh controls whether to do
+                a full retrieval of the catalog from the authority or only
+                update the existing catalog. auths is a list of authorities to
+                refresh. Passing an empty list or using the default value means
+                all known authorities will be refreshed. While it currently
+                returns an image object, this is an expedient for allowing
+                existing code to work while the rest of the API is put into
+                place."""
+
+        def info(self, fmri_strings, local, get_licenses):
+                """Gathers information about fmris. fmri_strings is a list
+                of fmri_names for which information is desired. local
+                determines whether to retrieve the information locally.
+                get_licenses determines whether to retrieve the text of
+                the licenses. It returns a dictionary of lists. The keys
+                for the dictionary are the constants specified in the class
+                definition. The values are lists of PackageInfo objects or
+                strings."""
+
+        def can_be_canceled(self):
+                """Returns true if the API is in a cancelable state."""
+
+        def reset(self):
+                """Resets the API back the the initial state. Note:
+                this does not necessarily return the disk to its initial state
+                since the indexes or download cache may have been changed by
+                the prepare method."""
+
+        def cancel(self):
+                """Used for asynchronous cancelation. It returns the API
+                to the state it was in prior to the current method being
+                invoked.  Canceling during a plan phase returns the API to
+                its initial state. Canceling during prepare puts the API
+                into the state it was in just after planning had completed.
+                Plan execution cannot be canceled. A call to this method blocks
+                until the canelation has happened. Note: this does not
+                necessarily return the disk to its initial state since the
+                indexes or download cache may have been changed by the
+                prepare method."""
+
+class PlanDescription(object):
+        """A class which describes the changes the plan will make. It
+        provides a list of tuples of PackageInfo's. The first item in the
+        tuple is the package that is being changed. The second item in the
+        tuple is the package that will be in the image after the change."""
+
+        def get_changes(self):
+
+class LicenseInfo(object):
+        """A class representing the license information a package
+        provides."""
+
+        def get_text(self):
+
+class PackageInfo(object):
+        """A class capturing the information about packages that a client
+        could need. The fmri is guaranteed to be set. All other values may
+        be None, depending on how the PackageInfo instance was created."""
+
+        # Possible package installation states
+        INSTALLED = 1
+        NOT_INSTALLED = 2
+
+        self.pkg_stem
+        self.summary
+        self.state
+        self.authority
+        self.preferred_authority
+        self.version
+        self.build_release
+        self.branch
+        self.packaging_date
+        self.size
+        self.fmri
+        self.licenses
+
+        def __str__(self):
--- a/src/client.py	Fri Nov 07 16:55:36 2008 -0800
+++ b/src/client.py	Sat Nov 08 11:08:13 2008 -0800
@@ -87,7 +87,7 @@
 from pkg.client.retrieve import CatalogRetrievalError
 from pkg.client.filelist import FileListRetrievalError
 
-CLIENT_API_VERSION = 5
+CLIENT_API_VERSION = 6
 PKG_CLIENT_NAME = "pkg"
 
 def error(text):
@@ -1039,9 +1039,18 @@
                 else:
                         raise RuntimeError("Encountered unknown package "
                             "information state: %d" % pi.state )
-                
-                msg(_("          Name:"), pi.pkg_stem)
+                name_str = _("          Name:")
+                msg(name_str, pi.pkg_stem)
                 msg(_("       Summary:"), pi.summary)
+                if pi.category_info_list:
+                        verbose = len(pi.category_info_list) > 1
+                        msg(_("      Category:"),
+                            pi.category_info_list[0].__str__(verbose))
+                        if len(pi.category_info_list) > 1:
+                                for ci in pi.category_info_list[1:]:
+                                        msg(" " * len(name_str),
+                                            ci.__str__(verbose))
+
                 msg(_("         State:"), state)
 
                 # XXX even more info on the authority would be nice?
--- a/src/modules/actions/attribute.py	Fri Nov 07 16:55:36 2008 -0800
+++ b/src/modules/actions/attribute.py	Sat Nov 08 11:08:13 2008 -0800
@@ -63,12 +63,13 @@
 
         def generate_indices(self):
                 """Generates the indices needed by the search dictionary."""
-                if self.attrs["name"] == "info.classification":
+                if self.has_category_info():
                         try:
-                                scheme, cats = self.attrs["value"].split(":", 1)
-                                return {
-                                    cats: cats.split("/") + [cats]
-                                }
+                                return dict(
+                                    (all_levels, [all_levels] + all_levels.split("/"))
+                                    for scheme, all_levels
+                                    in self.parse_category_info()
+                                )
                         except ValueError:
                                 pass
 
@@ -104,3 +105,16 @@
                         return {
                              self.attrs["value"]: self.attrs["value"]
                         }
+
+        def has_category_info(self):
+                return self.attrs["name"] == "info.classification"
+
+        def parse_category_info(self):
+                if not self.has_category_info():
+                        return []
+                else:
+                        try:
+                                scheme, cats = self.attrs["value"].split(":", 1)
+                                return [ (scheme, cats) ]
+                        except ValueError:
+                                return []
--- a/src/modules/client/api.py	Fri Nov 07 16:55:36 2008 -0800
+++ b/src/modules/client/api.py	Sat Nov 08 11:08:13 2008 -0800
@@ -33,7 +33,7 @@
 
 import threading
 
-CURRENT_API_VERSION = 5
+CURRENT_API_VERSION = 6
                 
 class ImageInterface(object):
         """This class presents an interface to images that clients may use.
@@ -68,7 +68,7 @@
                 canceled changes. It can raise VersionException and
                 ImageNotFoundException."""
                 
-                compatible_versions = set([1, 2, 3, 4, 5])
+                compatible_versions = set([1, 2, 3, 4, 5, 6])
                 
                 if version_id not in compatible_versions:
                         raise api_errors.VersionException(CURRENT_API_VERSION,
@@ -691,9 +691,15 @@
                                 dependencies = list(
                                     mfst.gen_key_attribute_value_by_type(
                                     "depend"))
+                        cat_info = [
+                            PackageCategory(scheme, cat)
+                            for ca in mfst.gen_actions_by_type("set")
+                            if ca.has_category_info()
+                            for scheme, cat in ca.parse_category_info()
+                        ]
 
                         pis.append(PackageInfo(pkg_stem=name, summary=summary,
-                            state=state,
+                            category_info_list=cat_info, state=state,
                             authority=authority,
                             preferred_authority=pref_auth,
                             version=version.release,
@@ -810,6 +816,17 @@
         def __str__(self):
                 return self.__text
 
+class PackageCategory(object):
+        def __init__(self, scheme, category):
+                self.scheme = scheme
+                self.category = category
+
+        def __str__(self, verbose=False):
+                if verbose:
+                        return "%s (%s)" % (self.category, self.scheme)
+                else:
+                        return "%s" % self.category
+        
 class PackageInfo(object):
         """A class capturing the information about packages that a client
         could need. The fmri is guaranteed to be set. All other values may
@@ -819,13 +836,17 @@
         INSTALLED = 1
         NOT_INSTALLED = 2
         
-        def __init__(self, pfmri, pkg_stem=None, summary=None, state=None,
-            authority=None, preferred_authority=None, version=None,
-            build_release=None, branch=None, packaging_date=None,
-            size=None, licenses=None, links=None, hardlinks=None, files=None,
-            dirs=None, dependencies=None):
+        def __init__(self, pfmri, pkg_stem=None, summary=None,
+            category_info_list=None, state=None, authority=None,
+            preferred_authority=None, version=None, build_release=None,
+            branch=None, packaging_date=None, size=None, licenses=None,
+            links=None, hardlinks=None, files=None, dirs=None,
+            dependencies=None):
                 self.pkg_stem = pkg_stem
                 self.summary = summary
+                if category_info_list is None:
+                        category_info_list = []
+                self.category_info_list = category_info_list
                 self.state = state
                 self.authority = authority
                 self.preferred_authority = preferred_authority
--- a/src/tests/cli/t_api_info.py	Fri Nov 07 16:55:36 2008 -0800
+++ b/src/tests/cli/t_api_info.py	Sat Nov 08 11:08:13 2008 -0800
@@ -37,7 +37,7 @@
 import pkg.client.api_errors as api_errors
 import pkg.client.progress as progress
 
-API_VERSION = 1
+API_VERSION = 6
 PKG_CLIENT_NAME = "pkg"
 
 class TestApiInfo(testutils.SingleDepotTestCase):
@@ -56,12 +56,14 @@
                 pkg1 = """
                     open [email protected],5.11-0
                     add dir mode=0755 owner=root group=bin path=/bin
+                    add set name=info.classification value="org.opensolaris.category.2008:Applications/Sound and Video
                     close
                 """
 
                 pkg2 = """
                     open [email protected],5.11-0
                     add dir mode=0755 owner=root group=bin path=/bin
+                    add set name=info.classification value=org.opensolaris.category.2008:System/Security/Foo/bar/Baz
                     close
                 """
 
@@ -93,6 +95,7 @@
                 self.assert_(pis[0].pkg_stem == 'jade')
                 self.assert_(len(notfound) == 2)
                 self.assert_(len(illegals) == 0)
+                self.assert_(len(pis[0].category_info_list) == 1)
 
                 ret = api_obj.info(["j*"], local, get_license)
                 pis = ret[api.ImageInterface.INFO_FOUND]
@@ -114,6 +117,7 @@
                 illegals = ret[api.ImageInterface.INFO_ILLEGALS]
                 self.assert_(len(pis) == 1)
                 self.assert_(pis[0].state == api.PackageInfo.INSTALLED)
+                self.assert_(len(pis[0].category_info_list) == 1)
 
                 ret = api_obj.info(["turquoise"], local, get_license)
                 pis = ret[api.ImageInterface.INFO_FOUND]
@@ -121,6 +125,7 @@
                 illegals = ret[api.ImageInterface.INFO_ILLEGALS]
                 self.assert_(len(pis) == 1)
                 self.assert_(pis[0].state == api.PackageInfo.NOT_INSTALLED)
+                self.assert_(len(pis[0].category_info_list) == 1)
 
                 ret = api_obj.info(["emerald"], local, get_license)
                 pis = ret[api.ImageInterface.INFO_FOUND]
--- a/src/tests/cli/t_pkg_info.py	Fri Nov 07 16:55:36 2008 -0800
+++ b/src/tests/cli/t_pkg_info.py	Sat Nov 08 11:08:13 2008 -0800
@@ -127,12 +127,14 @@
                 pkg1 = """
                     open [email protected],5.11-0
                     add dir mode=0755 owner=root group=bin path=/bin
+                    add set name=info.classification value="org.opensolaris.category.2008:Applications/Sound and Video
                     close
                 """
 
                 pkg2 = """
                     open [email protected],5.11-0
                     add dir mode=0755 owner=root group=bin path=/bin
+                    add set name=info.classification value=org.opensolaris.category.2008:System/Security/Foo/bar/Baz
                     close
                 """
 
@@ -149,6 +151,8 @@
                 
                 # Check local info
                 self.pkg("info jade | grep 'State: Installed'")
+                self.pkg("info jade | grep '      Category: Applications/Sound and Video'")
+                self.pkg("info jade | grep '      Category: Applications/Sound and Video (org.opensolaris.category.2008)'", exit=1)
                 self.pkg("info turquoise 2>&1 | grep 'no packages matching'")
                 self.pkg("info emerald", exit=1)
                 self.pkg("info emerald 2>&1 | grep 'no packages matching'")
@@ -157,7 +161,11 @@
 
                 # Check remote info
                 self.pkg("info -r jade | grep 'State: Installed'")
-                self.pkg("info -r turquoise| grep 'State: Not installed'")
+                self.pkg("info -r jade | grep '      Category: Applications/Sound and Video'")
+                self.pkg("info -r jade | grep '      Category: Applications/Sound and Video (org.opensolaris.category.2008)'", exit=1)
+                self.pkg("info -r turquoise | grep 'State: Not installed'")
+                self.pkg("info -r turquoise | grep '      Category: System/Security/Foo/bar/Baz'")
+                self.pkg("info -r turquoise | grep '      Category: System/Security/Foo/bar/Baz (org.opensolaris.category.2008)'", exit=1)
                 self.pkg("info -r emerald", exit=1)
                 self.pkg("info -r emerald 2>&1 | grep 'no packages matching'")