diff --git a/checkbox-ng/plainbox/__init__.py b/checkbox-ng/plainbox/__init__.py index 3f9e090012..b86fe4609f 100644 --- a/checkbox-ng/plainbox/__init__.py +++ b/checkbox-ng/plainbox/__init__.py @@ -54,3 +54,62 @@ def get_version_string(): else: version_string = "{} {}".format("Checkbox", __version__) return version_string + + +def get_origin(): + """ + Return a dictionary containing information such as the version and what + packaging method is being used (Python virtual environment, Snap or + Debian). + """ + import os + import subprocess + + if os.getenv("SNAP_NAME"): + origin = { + "name": "Checkbox", + "version": __version__, + "packaging": { + "type": "snap", + "name": os.getenv("SNAP_NAME"), + "version": os.getenv("SNAP_VERSION"), + "revision": os.getenv("SNAP_REVISION"), + }, + } + elif os.getenv("VIRTUAL_ENV"): + origin = { + "name": "Checkbox", + "version": __version__, + "packaging": { + "type": "source", + "version": __version__, + }, + } + else: + try: + dpkg_info = subprocess.check_output( + ["dpkg", "-S", __path__[0]], universal_newlines=True + ) + # 'python3-checkbox-ng: /usr/lib/python3/dist-packages/plainbox\n' + package_name = dpkg_info.split(":")[0] + origin = { + "name": "Checkbox", + "version": __version__, + "packaging": { + "type": "debian", + "name": package_name, + "version": __version__, + }, + } + # if all of the above failed and dpkg is not available on the system... + except (FileNotFoundError, subprocess.CalledProcessError): + origin = { + "name": "Checkbox", + "version": __version__, + "packaging": { + "type": "unknown", + "name": "unknown", + "version": "unknown", + }, + } + return origin diff --git a/checkbox-ng/plainbox/impl/exporter/jinja2.py b/checkbox-ng/plainbox/impl/exporter/jinja2.py index 78081e3535..336529114c 100644 --- a/checkbox-ng/plainbox/impl/exporter/jinja2.py +++ b/checkbox-ng/plainbox/impl/exporter/jinja2.py @@ -45,7 +45,7 @@ from plainbox import get_version_string -from plainbox import __version__ as checkbox_version +from plainbox import get_origin from plainbox.abc import ISessionStateExporter from plainbox.impl.exporter import SessionStateExporterBase from plainbox.impl.result import OUTCOME_METADATA_MAP @@ -94,6 +94,7 @@ def __init__( timestamp=None, client_version=None, client_name="plainbox", + origin=None, exporter_unit=None, ): """ @@ -110,7 +111,7 @@ def __init__( self._client_version = client_version or get_version_string() # Remember client name self._client_name = client_name - self._checkbox_version = checkbox_version + self._origin = origin or get_origin() self.option_list = None self.template = None @@ -197,7 +198,7 @@ def dump_from_session_manager(self, session_manager, stream): "OUTCOME_METADATA_MAP": OUTCOME_METADATA_MAP, "client_name": self._client_name, "client_version": self._client_version, - "checkbox_version": self._checkbox_version, + "origin": self._origin, "manager": session_manager, "app_blob": app_blob_data, "options": self.option_list, @@ -223,7 +224,7 @@ def dump_from_session_manager_list(self, session_manager_list, stream): "OUTCOME_METADATA_MAP": OUTCOME_METADATA_MAP, "client_name": self._client_name, "client_version": self._client_version, - "checkbox_version": self._checkbox_version, + "origin": self._origin, "manager_list": session_manager_list, "app_blob": {}, "options": self.option_list, diff --git a/checkbox-ng/plainbox/impl/exporter/test_html.py b/checkbox-ng/plainbox/impl/exporter/test_html.py index 7cce67b74e..51e8a3f9d6 100644 --- a/checkbox-ng/plainbox/impl/exporter/test_html.py +++ b/checkbox-ng/plainbox/impl/exporter/test_html.py @@ -179,6 +179,11 @@ def test_perfect_match_without_certification_status(self): system_id="", timestamp="2012-12-21T12:00:00", client_version="Checkbox 1.0", + origin={ + "name": "Checkbox", + "version": "1.0", + "packaging": {"type": "source"}, + }, exporter_unit=self.exporter_unit, ) stream = io.BytesIO() @@ -202,6 +207,11 @@ def test_perfect_match_with_certification_blocker(self): system_id="", timestamp="2012-12-21T12:00:00", client_version="Checkbox 1.0", + origin={ + "name": "Checkbox", + "version": "1.0", + "packaging": {"type": "source"}, + }, exporter_unit=self.exporter_unit, ) stream = io.BytesIO() diff --git a/checkbox-ng/plainbox/impl/exporter/test_jinja2.py b/checkbox-ng/plainbox/impl/exporter/test_jinja2.py index 2c7e0bde2e..120c7f3cc8 100644 --- a/checkbox-ng/plainbox/impl/exporter/test_jinja2.py +++ b/checkbox-ng/plainbox/impl/exporter/test_jinja2.py @@ -76,7 +76,14 @@ def test_template(self): exporter_unit.option_list = () with open(pathname, "w") as f: f.write(tmpl) - exporter = Jinja2SessionStateExporter(exporter_unit=exporter_unit) + exporter = Jinja2SessionStateExporter( + origin={ + "name": "Checkbox", + "version": "1.0", + "packaging": {"type": "source"}, + }, + exporter_unit=exporter_unit, + ) stream = BytesIO() exporter.dump_from_session_manager(self.manager_single_job, stream) expected_bytes = " fail : job name\n".encode("UTF-8") @@ -95,7 +102,14 @@ def test_validation_chooses_json(self): exporter_unit.data_dir = tmp exporter_unit.template = template_filename exporter_unit.option_list = () - exporter = Jinja2SessionStateExporter(exporter_unit=exporter_unit) + exporter = Jinja2SessionStateExporter( + origin={ + "name": "Checkbox", + "version": "1.0", + "packaging": {"type": "source"}, + }, + exporter_unit=exporter_unit, + ) exporter.validate_json = mock.Mock(return_value=[]) stream = BytesIO() exporter.validate(stream) @@ -114,7 +128,14 @@ def test_validation_json(self): exporter_unit.data_dir = tmp exporter_unit.template = template_filename exporter_unit.option_list = () - exporter = Jinja2SessionStateExporter(exporter_unit=exporter_unit) + exporter = Jinja2SessionStateExporter( + origin={ + "name": "Checkbox", + "version": "1.0", + "packaging": {"type": "source"}, + }, + exporter_unit=exporter_unit, + ) stream = BytesIO() exporter.dump_from_session_manager(self.manager_single_job, stream) @@ -131,7 +152,14 @@ def test_validation_json_throws(self): exporter_unit.data_dir = tmp exporter_unit.template = template_filename exporter_unit.option_list = () - exporter = Jinja2SessionStateExporter(exporter_unit=exporter_unit) + exporter = Jinja2SessionStateExporter( + origin={ + "name": "Checkbox", + "version": "1.0", + "packaging": {"type": "source"}, + }, + exporter_unit=exporter_unit, + ) stream = BytesIO() with self.assertRaises(ExporterError): exporter.dump_from_session_manager( diff --git a/checkbox-ng/plainbox/impl/providers/exporters/data/checkbox.json b/checkbox-ng/plainbox/impl/providers/exporters/data/checkbox.json index 53a1673e14..9ec926080b 100644 --- a/checkbox-ng/plainbox/impl/providers/exporters/data/checkbox.json +++ b/checkbox-ng/plainbox/impl/providers/exporters/data/checkbox.json @@ -6,8 +6,7 @@ {%- set system_information = state.system_information -%} { "title": {{ state.metadata.title | jsonify | safe }}, - "client_version": {{ client_version | jsonify | safe }}, - "checkbox_version": {{ checkbox_version | jsonify | safe }}, + "origin": {{ origin | jsonify | safe }}, {%- if "testplan_id" in app_blob %} {%- if app_blob['testplan_id'] %} "testplan_id": {{ app_blob['testplan_id'] | jsonify | safe }}, diff --git a/checkbox-ng/plainbox/test_init.py b/checkbox-ng/plainbox/test_init.py new file mode 100644 index 0000000000..d947808848 --- /dev/null +++ b/checkbox-ng/plainbox/test_init.py @@ -0,0 +1,56 @@ +# This file is part of Checkbox. +# +# Copyright 2024 Canonical Ltd. +# Written by: +# Pierre Equoy +# +# Checkbox is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 3, +# as published by the Free Software Foundation. +# +# Checkbox is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Checkbox. If not, see . + +from unittest import TestCase, mock + +import os +from subprocess import CalledProcessError + +import plainbox + + +class PlainboxInitTests(TestCase): + @mock.patch.dict(os.environ, {"VIRTUAL_ENV": "test"}) + def test_get_origin_venv(self): + origin = plainbox.get_origin() + self.assertEqual(origin["packaging"]["type"], "source") + + @mock.patch.dict(os.environ, {"SNAP_NAME": "test"}) + def test_get_origin_snap(self): + origin = plainbox.get_origin() + self.assertEqual(origin["packaging"]["type"], "snap") + + @mock.patch.dict(os.environ, {}, clear=True) + @mock.patch("subprocess.check_output") + def test_get_origin_debian(self, mock_sp_check_output): + mock_sp_check_output.return_value = ( + "python3-checkbox-ng: /usr/lib/python3/dist-packages/plainbox\n" + ) + origin = plainbox.get_origin() + self.assertEqual(origin["packaging"]["type"], "debian") + self.assertEqual(origin["packaging"]["name"], "python3-checkbox-ng") + + @mock.patch.dict(os.environ, {}, clear=True) + @mock.patch("subprocess.check_output") + def test_get_origin_exception(self, mock_sp_check_output): + mock_sp_check_output.side_effect = FileNotFoundError + origin = plainbox.get_origin() + self.assertEqual(origin["packaging"]["type"], "unknown") + mock_sp_check_output.side_effect = CalledProcessError(1, "error") + origin = plainbox.get_origin() + self.assertEqual(origin["packaging"]["type"], "unknown") diff --git a/submission-schema/schema.json b/submission-schema/schema.json index baaa98aa0e..955e144a79 100644 --- a/submission-schema/schema.json +++ b/submission-schema/schema.json @@ -1,5 +1,5 @@ { - "$schema": "http://json-schema.org/draft-06/schema#", + "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://certification.canonical.com/checkbox-submission.json", "description": "Output format for the Checkbox test framework (see more info at https://checkbox.readthedocs.io)", "$ref": "#/definitions/Submission", @@ -11,12 +11,51 @@ "title": { "type": "string" }, - "client_version": { - "type": "string" - }, - "checkbox_version": { - "type": "string" - }, + "origin": { + "type": "object", + "required": [ + "name", + "version", + "packaging" + ], + "properties": { + "name": { + "type": "string", + "description": "Name of the application used to run the test plan ('Checkbox')" + }, + "version": { + "type": "string", + "description": "Version of Checkbox used to run the test plan." + }, + "packaging": { + "type": "object", + "required": [ + "type", + "version" + ], + "properties": { + "type": { + "type": "string", + "description": "Packaging type ('debian', 'snap', or 'source' if running in a Python virtual env" + }, + "name": { + "type": "string", + "description": "Name of the package used, if any" + }, + "version": { + "type": "string", + "description": "Version installed" + }, + "revision": { + "type": "string", + "description": "Revision installed (only for Snaps)" + } + }, + "if": {"properties": {"type": {"const": "snap"}}}, + "then": {"required": ["revision"]} + } + } + }, "testplan_id": { "type": "string" },