PGMR: More general flexibility for os compat check

* Don't restrict the list of compatibility values to check against
    to only those we have mapped, also support unmapped more exotic
    ones.

  * Allow 1:1 check against sys.platform values (with startswith).
    Combined with the above that allows very granular compatibility
    modelling ("freebsd11", "freebsd12") if required.

  * Instead of only whitelisting ("linux", "freebsd") now also black
    listing is possible ("!windows").

    A detected os must match all provided whitelist elements (if the
    whitelist is empty that is considered always the case) and none of
    the backlist elements (if the blacklist is empty that is also
    considered always the case).

See the included unit tests for examples of how this works.
This commit is contained in:
Gina Häußge 2017-04-07 09:09:56 +02:00
parent 956d9581c5
commit 346f818707
5 changed files with 101 additions and 8 deletions

View file

@ -571,6 +571,10 @@ class PluginManager(object):
key = entry.name
elif entry.is_file() and entry.name.endswith(".py"):
key = entry.name[:-3] # strip off the .py extension
if key.startswith("__"):
# might be an __init__.py in our plugins folder, or something else we don't want
# to handle
continue
else:
continue

View file

@ -0,0 +1 @@
# make our plugins testable

View file

@ -808,20 +808,37 @@ class PluginManagerPlugin(octoprint.plugin.SimpleApiPlugin,
return True
def _is_os_compatible(self, current_os, compatibility_entries):
@staticmethod
def _is_os_compatible(current_os, compatibility_entries):
"""
Tests if the ``current_os`` or ``sys.platform`` matches any of the provided ``compatibility_entries``.
Tests if the ``current_os`` or ``sys.platform`` are blacklisted or whitelisted in ``compatibility_entries``
"""
general_match = current_os in filter(lambda x: x in self.__class__.OPERATING_SYSTEMS.keys(), compatibility_entries)
exact_match = sys.platform in compatibility_entries
return general_match or exact_match
if len(compatibility_entries) == 0:
# shortcut - no compatibility info means we are compatible
return True
def _get_os(self):
for identifier, platforms in self.__class__.OPERATING_SYSTEMS.items():
negative_entries = map(lambda x: x[1:], filter(lambda x: x.startswith("!"), compatibility_entries))
positive_entries = filter(lambda x: not x.startswith("!"), compatibility_entries)
negative_match = False
if negative_entries:
# check if we are blacklisted
negative_match = current_os in negative_entries or any(map(lambda x: sys.platform.startswith(x), negative_entries))
positive_match = True
if positive_entries:
# check if we are whitelisted
positive_match = current_os in positive_entries or any(map(lambda x: sys.platform.startswith(x), positive_entries))
return positive_match and not negative_match
@classmethod
def _get_os(cls):
for identifier, platforms in cls.OPERATING_SYSTEMS.items():
if (callable(platforms) and platforms(sys.platform)) or (isinstance(platforms, list) and sys.platform in platforms):
return identifier
else:
return "unknown"
return "unmapped"
def _get_octoprint_version_string(self):
return VERSION

10
tests/plugins/__init__.py Normal file
View file

@ -0,0 +1,10 @@
# coding=utf-8
"""
Unit tests for bundled plugins.
"""
from __future__ import absolute_import
__author__ = "Gina Häußge <osd@foosel.net>"
__license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agpl.html'
__copyright__ = "Copyright (C) 2017 The OctoPrint Project - Released under terms of the AGPLv3 License"

View file

@ -0,0 +1,61 @@
# coding=utf-8
"""
Unit tests for bundled plugin "PluginManager".
"""
from __future__ import absolute_import
__author__ = "Gina Häußge <osd@foosel.net>"
__license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agpl.html'
__copyright__ = "Copyright (C) 2017 The OctoPrint Project - Released under terms of the AGPLv3 License"
import unittest
import mock
import ddt
from octoprint.plugins.pluginmanager import PluginManagerPlugin
@ddt.ddt
class PluginManagerPluginTests(unittest.TestCase):
@ddt.data(
("linux", "linux2", [], True),
("linux", "linux2", ["linux", "freebsd"], True),
("windows", "win32", ["linux", "freebsd"], False),
("linux", "linux2", ["!windows"], True),
("windows", "win32", ["!windows"], False),
("unmapped", "os2", [], True),
("unmapped", "os2", ["linux", "freebsd"], False),
("unmapped", "os2", ["!os2"], False),
("unmapped", "sunos5", ["linux", "freebsd", "sunos"], True),
("unmapped", "sunos5", ["!sunos", "!os2"], False),
# both black and white listing at the same time usually doesn't
# make a whole lot of sense, but let's test it anyhow
("linux", "linux2", ["!windows", "linux", "freebsd"], True),
("linux", "linux2", ["!windows", "freebsd"], False),
("windows", "win32", ["!windows", "linux", "freebsd"], False),
("unmapped", "sunos5", ["!windows", "linux", "freebsd"], False)
)
@ddt.unpack
def test_is_os_compatible(self, current_os, sys_platform, entries, expected):
with mock.patch("sys.platform", sys_platform):
actual = PluginManagerPlugin._is_os_compatible(current_os, entries)
self.assertEqual(actual, expected)
@ddt.data(
("win32", "windows"),
("linux2", "linux"),
("darwin", "macos"),
("linux", "linux"),
("linux3", "linux"),
("freebsd", "freebsd"),
("freebsd2342", "freebsd"),
("os2", "unmapped"),
("sunos5", "unmapped")
)
@ddt.unpack
def test_get_os(self, sys_platform, expected):
with mock.patch("sys.platform", sys_platform):
actual = PluginManagerPlugin._get_os()
self.assertEqual(actual, expected)