diff --git a/tests/README b/tests/README new file mode 100644 index 0000000..59f6e7a --- /dev/null +++ b/tests/README @@ -0,0 +1,36 @@ +Tests can be started with "make test" command from parent directory +or by executing "tuned-test.py" script. + +The tests can not be run in completely isolated environment. You need +tuned and tuned-utils packages installed and you have to run the test +as superuser (needed for tuned-adm tests). Please note that your +settings of tuned can be changed. + +What is tested + + * monitor-plugins have to implement: init(), getLoad(), cleanup() + * tuning-plugins have to implement: init(), setTuning(), cleanup() + * each monitor-plugin has to have it's tuning-plugin with the same + name + * the result of monitor's getLoad() function is passed to setTuning(), + therefore tuning's getLoad() must not return Null + * "tuned-adm list" correctness + * "tuned-adm off" has to: + - stop tuned a ktune services + - remove (disable) tuned a ktune services + - remove "/etc/ktune.d/tunedadm.{sh,conf}" + * there are fake-profiles available for "tuned-adm profile ": + - disabled-all + - enabled-all + - enabled-ktune + - enabled-tuned + tuned-adm has to: + - stop/start service(s) + - add/remove/disable service(s) + - set up "/etc/ktune.d/tunedadm.{sh,conf}" + +What is NOT tested + + * monitor-plugins getLoad() data validity + * tuning-plugins setTuning() system setting changes + diff --git a/tests/TODO b/tests/TODO new file mode 100644 index 0000000..67331c0 --- /dev/null +++ b/tests/TODO @@ -0,0 +1,2 @@ +- hiding output of tuned-adm (restarting services, etc..) + diff --git a/tests/admtest.py b/tests/admtest.py new file mode 100644 index 0000000..25c49b0 --- /dev/null +++ b/tests/admtest.py @@ -0,0 +1,183 @@ +# +# Copyright (C) 2008, 2009 Red Hat, Inc. +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program 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 this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# + +import os, sys, re +from logging import log +from streamcapture import capture + +class AdmTester: + + def __init__(self): + self.profiles_dir = os.path.normpath(os.path.dirname(__file__) + "/../tune-profiles") + self.profiles_fake_dir = os.path.normpath(os.path.dirname(__file__) + "/fake-profiles") + + def run(self): + import tuned_adm + self.tuned_adm = tuned_adm.tuned_adm + + self.tuned_adm.init(self.profiles_dir) + + self.__check_profiles() + self.__check_privileges() + + # profiles switching + self.tuned_adm.init(self.profiles_fake_dir) + + testing_states = [ + #[ None, False, False ], # off mode + [ "enabled-all", True, True ], + [ None, False, False ], # off mode + [ "enabled-tuned", True, False ], + [ "enabled-ktune", False, True ], + [ "disabled-all", False, False ] + ] + + for ts in testing_states: + self.__check_state(ts[0], ts[1], ts[2]) + + def __check_profiles(self): + log.test("profiles listing") + + profiles_tester = os.listdir(self.profiles_dir) + profiles_tester = filter(lambda f: f[0] != ".", profiles_tester) + + try: + capture.clean() + capture.capture() + self.tuned_adm.run(["list"]) + capture.stdout() + except Exception as e: + log.report_e(e) + return False + + # printed profiles (first line "Modes:" is skipped) + profiles_tuned = capture.getcaptured().splitlines()[1:] + + # compare profiles_tester and profiles_tuned + + error = False + for p in profiles_tester: + if not p in profiles_tuned: + log.info("tune-adm does not report profile '%s'" % p) + error = True + + for p in profiles_tuned: + if not p in profiles_tester: + log.info("tune-adm reports extra profile '%s'" % p) + error = True + + if error: + log.result("profiles detected by this test differ from these reported by tuned-adm") + else: + log.result() + + return True + + def __check_privileges(self): + log.test("privileges") + if os.getuid() != 0: + log.result("You have to be root to run all following tests.") + return False + else: + log.result() + return True + + def __service_running(self, service): + status = os.system("service %s status 1>/dev/null 2>&1" % service) + # 0 running, 3 stopped + if status == 0: + return True + else: + return False + + def __initscript_enabled(self, service): + if os.system('chkconfig | grep -qx "^%s\W.*on.*$"' % service) == 0: + return True + else: + return False + + def __check_state(self, profile, tuned_running, ktune_running): + is_ok = True + + if profile == None: + self.tuned_adm.run(["off"]) + log.test("checking state: off") + else: + self.tuned_adm.run(["profile", profile]) + log.test("checking state: %s" % profile) + + log.indent() + + # services status + + log.test("service 'tuned'") + if self.__service_running("tuned") == tuned_running: + log.result() + else: + is_ok = False + log.result("should %s" % ( "be running" if tuned_running else "not be running" )) + + log.test("service 'ktune'") + if self.__service_running("ktune") == ktune_running: + log.result() + else: + is_ok = False + log.result("should %s" % ( "be running" if ktune_running else "not be running" )) + + # init scripts + + log.test("checking 'tuned' initscript") + if self.__initscript_enabled("tuned") == tuned_running: + log.result() + else: + is_ok = False + log.result("%s be enabled" % ( "should" if tuned_running else "should not" )) + + log.test("checking 'ktune' initscript") + if self.__initscript_enabled("ktune") == ktune_running: + log.result() + else: + is_ok = False + log.result("%s be enabled" % ( "should" if ktune_running else "should not" )) + + # config files + + want_tunedadm_sh = False + want_tunedadm_conf = False + + if profile != None: + want_tunedadm_sh = os.path.exists("%s/%s/ktune.sh" % (self.profiles_fake_dir, profile)) + want_tunedadm_conf = os.path.exists("%s/%s/sysctl.ktune" % (self.profiles_fake_dir, profile)) + + log.test("checking '/etc/ktune.d/tunedadm.sh'") + if want_tunedadm_sh == os.path.exists("/etc/ktune.d/tunedadm.sh"): + log.result() + else: + is_ok = False + log.result("file should %s" % ( "exist" if want_tunedadm_sh else "not exist" )) + + log.test("checking '/etc/ktune.d/tunedadm.conf'") + if want_tunedadm_conf == os.path.exists("/etc/ktune.d/tunedadm.conf"): + log.result() + else: + is_ok = False + log.result("file should %s" % ( "exist" if want_tunedadm_conf else "not exist" )) + + log.unindent() + return is_ok + diff --git a/tests/fake-profiles/enabled-all/ktune.sh b/tests/fake-profiles/enabled-all/ktune.sh new file mode 100644 index 0000000..e69de29 diff --git a/tests/fake-profiles/enabled-all/ktune.sysconfig b/tests/fake-profiles/enabled-all/ktune.sysconfig new file mode 100644 index 0000000..9529911 --- /dev/null +++ b/tests/fake-profiles/enabled-all/ktune.sysconfig @@ -0,0 +1,27 @@ +# ktune service configuration + +# This is the ktune sysctl file. You can comment this out to prevent ktune +# from applying its sysctl settings. +SYSCTL="/etc/sysctl.ktune" + +# Use *.conf files in the ktune configuration directory /etc/ktune.d. +# Value: yes|no, default: yes +# It is useful if you want to load settings from additional files. Set this to +# no if you to prevent ktune from using these additional files. +USE_KTUNE_D="yes" + +# This is the custom sysctl configuration file. Any settings in this file will +# be applied after the ktune settings, overriding them. Comment this out to +# use only the ktune settings. +SYSCTL_POST="/etc/sysctl.conf" + +# This is the I/O scheduler ktune will use. This will *not* override anything +# explicitly set on the kernel command line, nor will it change the scheduler +# for any block device that is using a non-default scheduler when ktune starts. +# You should probably leave this on "deadline", but "as", "cfq", and "noop" are +# also legal values. Comment this out to prevent ktune from changing I/O +# scheduler settings. +#ELEVATOR="deadline" + +# These are the devices, that should be tuned with the ELEVATOR +ELEVATOR_TUNE_DEVS="" diff --git a/tests/fake-profiles/enabled-all/sysctl.ktune b/tests/fake-profiles/enabled-all/sysctl.ktune new file mode 100644 index 0000000..9474bba --- /dev/null +++ b/tests/fake-profiles/enabled-all/sysctl.ktune @@ -0,0 +1 @@ +# ktune sysctl diff --git a/tests/fake-profiles/enabled-all/tuned.conf b/tests/fake-profiles/enabled-all/tuned.conf new file mode 100644 index 0000000..f0fc09a --- /dev/null +++ b/tests/fake-profiles/enabled-all/tuned.conf @@ -0,0 +1,2 @@ +[main] +interval=10 diff --git a/tests/fake-profiles/enabled-ktune/ktune.sh b/tests/fake-profiles/enabled-ktune/ktune.sh new file mode 100644 index 0000000..e69de29 diff --git a/tests/fake-profiles/enabled-ktune/ktune.sysconfig b/tests/fake-profiles/enabled-ktune/ktune.sysconfig new file mode 100644 index 0000000..9529911 --- /dev/null +++ b/tests/fake-profiles/enabled-ktune/ktune.sysconfig @@ -0,0 +1,27 @@ +# ktune service configuration + +# This is the ktune sysctl file. You can comment this out to prevent ktune +# from applying its sysctl settings. +SYSCTL="/etc/sysctl.ktune" + +# Use *.conf files in the ktune configuration directory /etc/ktune.d. +# Value: yes|no, default: yes +# It is useful if you want to load settings from additional files. Set this to +# no if you to prevent ktune from using these additional files. +USE_KTUNE_D="yes" + +# This is the custom sysctl configuration file. Any settings in this file will +# be applied after the ktune settings, overriding them. Comment this out to +# use only the ktune settings. +SYSCTL_POST="/etc/sysctl.conf" + +# This is the I/O scheduler ktune will use. This will *not* override anything +# explicitly set on the kernel command line, nor will it change the scheduler +# for any block device that is using a non-default scheduler when ktune starts. +# You should probably leave this on "deadline", but "as", "cfq", and "noop" are +# also legal values. Comment this out to prevent ktune from changing I/O +# scheduler settings. +#ELEVATOR="deadline" + +# These are the devices, that should be tuned with the ELEVATOR +ELEVATOR_TUNE_DEVS="" diff --git a/tests/fake-profiles/enabled-ktune/sysctl.ktune b/tests/fake-profiles/enabled-ktune/sysctl.ktune new file mode 100644 index 0000000..9474bba --- /dev/null +++ b/tests/fake-profiles/enabled-ktune/sysctl.ktune @@ -0,0 +1 @@ +# ktune sysctl diff --git a/tests/fake-profiles/enabled-tuned/tuned.conf b/tests/fake-profiles/enabled-tuned/tuned.conf new file mode 100644 index 0000000..f0fc09a --- /dev/null +++ b/tests/fake-profiles/enabled-tuned/tuned.conf @@ -0,0 +1,2 @@ +[main] +interval=10 diff --git a/tests/logging.py b/tests/logging.py new file mode 100644 index 0000000..5ff1afd --- /dev/null +++ b/tests/logging.py @@ -0,0 +1,92 @@ +# +# Copyright (C) 2008, 2009 Red Hat, Inc. +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program 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 this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# + +class Logging: + + __indent = 0 + __testFinished = True + __info = [] + + def __init__(self): + pass + + def end(self): + if not self.__testFinished: + print + self.__infoFlush() + self.__indent = 0 + print + + def section(self, info): + print + print "== %s ==" % info + print + self.__indent = 0 + + def indent(self): + self.__indent += 1 + + def unindent(self): + self.__indent -= 1 + + def test(self, info): + if not self.__testFinished: + print + self.__infoFlush() + + self.__testFinished = False + i = self.__indent + self.__info = [] + + if i == 0: bullet = "* " + elif i == 1: bullet = "+ " + elif i == 2: bullet = "- " + else: bullet = "? " + + print (i * " ") + bullet + info, + + def info(self, info): + self.__info.append(str(info)) + if self.__testFinished: + self.__infoFlush() + + def __infoFlush(self): + for i in self.__info: + print ((self.__indent + 1) * " ") + "> " + i + self.__info = [] + + def result(self, failinfo = None): + if failinfo == None: + print ": success" + else: + print ": failed" + self.info(failinfo) + + self.__infoFlush() + self.__testFinished = True + + def result_e(self, exception): + print ": failed" + + if not exception == None: + self.info("Exception: %s, %s" % (exception, type(exception))) + self.__infoFlush() + self.__testFinished = True + +log = Logging() + diff --git a/tests/pluginstest.py b/tests/pluginstest.py index 73fd26b..c83a023 100644 --- a/tests/pluginstest.py +++ b/tests/pluginstest.py @@ -17,6 +17,7 @@ # import os, ConfigParser +from logging import log class PluginsTester: @@ -27,12 +28,14 @@ class PluginsTester: self.tp_dir = "tuningplugins" self.config = ConfigParser.RawConfigParser() + self.config.add_section("main") + self.config.set("main", "interval", 10) def run(self): - self.__checkPlugins() + self.__check_plugins() - def __getPluginsFromDir(self, dir): + def __get_plugins_from_dir(self, dir): files = os.listdir(self.tunedir + "/" + dir); plugins = filter(lambda f: f[0] != "." and f[-3:] == ".py" and f != "__init__.py", files) @@ -41,176 +44,178 @@ class PluginsTester: return plugins - ### reporting tests progress + def __check_plugins(self): - def __reportTestBegin(self, info): - print " * %s" % (info) - - def __reportTestStep(self, info): - self.__testStep = info - - def __reportTestResult(self, exception = None): - if exception == None: - print " - %s: success" % (self.__testStep) - else: - print " - %s: failed (%s, %s)" % (self.__testStep, exception, type (exception)) - def __reportTestSkip(self, info): - print " - %s: skipped, %s" % (self.__testStep, info) - - ### testing - - def __checkPlugins(self): - - monitorplugins = self.__getPluginsFromDir(self.mp_dir) - tuningplugins = self.__getPluginsFromDir(self.tp_dir) + monitorplugins = self.__get_plugins_from_dir(self.mp_dir) + tuningplugins = self.__get_plugins_from_dir(self.tp_dir) # check plugins availability - print "checking plugins availablity" - - self.__checkSiblingPlugins(monitorplugins, tuningplugins) - - print + self.__check_sibling_plugins(monitorplugins, tuningplugins) # monitor plugins test - print "monitor plugins test" + log.test("monitor plugins test") if len(monitorplugins) == 0: - print " - no plugins found" + log.result("no plugins found") + + log.indent() monitor_results = {} - for mp in monitorplugins: - load = self.__testMonitorPlugin(mp) + load = self.__test_monitor_plugin(mp) monitor_results[mp] = load - print + log.unindent() # tuning plugins test - print "tunning plugins test" + log.test("tunning plugins test") + if len(tuningplugins) == 0: - print " - no plugins found" + log.result("no plugins found") + + log.indent() for tp in tuningplugins: try: load = monitor_results[tp] except: load = None - self.__testTuningPlugin(tp, load) + self.__test_tuning_plugin(tp, load) - def __checkSiblingPlugins(self, monitorplugins, tuningplugins): + log.unindent() + + def __check_sibling_plugins(self, monitorplugins, tuningplugins): ok = True + log.test("monitor and tuning plugins availability") + for mp in monitorplugins: if tuningplugins.count(mp) != 1: ok = False - print " - monitor plugin '%s' misses tuning plugin" % mp + log.info("monitor plugin '%s' misses tuning plugin" % mp) for tp in tuningplugins: if monitorplugins.count(tp) != 1: ok = False - print " - tuning plugin '%s' misses monitor plugin" % tp + log.info("tuning plugin '%s' misses monitor plugin" % tp) if ok: - print " - monitor and tuning plugins match" + log.result() + else: + log.result("monitor and tunning plugins do not match") - def __testMonitorPlugin(self, name): + def __test_monitor_plugin(self, name): - self.__reportTestBegin("monitor plugin: %s" % (name)) + log.test("monitor plugin: %s" % name) + log.indent() # initialization - self.__reportTestStep("initialization") + log.test("initialization") try: exec "from %s.%s import _plugin" % (self.mp_dir, name) except Exception as e: - self.__reportTestResult(e) + log.result_e(e) + log.unindent() return None - self.__reportTestResult() + + log.result() # init() - self.__reportTestStep("call init()") + log.test("call init()") try: _plugin.init(self.config) except Exception as e: - self.__reportTestResult(e) + log.result_e(e) + log.unindent() return None - self.__reportTestResult() + log.result() # getLoad() - self.__reportTestStep("call getLoad()") + log.test("call getLoad()") try: load = _plugin.getLoad() if load == None: raise Exception("Plugin returned None as a result.") except Exception as e: - self.__reportTestResult(e) + log.result_e(e) + log.unindent() return None - self.__reportTestResult() + log.result() # cleanup() - self.__reportTestStep("call cleanup()") + log.test("call cleanup()") try: _plugin.cleanup() except Exception as e: - self.__reportTestResult(e) - return None - self.__reportTestResult() + log.result_e(e) + log.unindent() + return load + log.result() + + log.unindent() return load - def __testTuningPlugin(self, name, load): + def __test_tuning_plugin(self, name, load): - self.__reportTestBegin("tuning plugin: %s" % (name)) + log.test("tuning plugin: %s" % name) + log.indent() # initialization - self.__reportTestStep("initialization") + log.test("initialization") try: exec "from %s.%s import _plugin" % (self.tp_dir, name) except Exception as e: - self.__reportTestResult(e) + log.result_e() + log.unindent() return False - self.__reportTestResult() + log.result() # init() - self.__reportTestStep("call init()") + log.test("call init()") try: _plugin.init(self.config) except Exception as e: - self.__reportTestResult(e) + log.result_e(e) + log.unindent() return False - self.__reportTestResult() + log.result() # setTuning() - self.__reportTestStep("call setTuning()") + log.test("call setTuning()") if load == None: - self.__reportTestSkip("no data from monitor plugin available") + log.info("no data from monitor plugin available") else: - try: - _plugin.setTuning(load) - except Exception as e: - self.__reportTestResult(e) - return False - self.__reportTestResult() + try: + _plugin.setTuning(load) + except Exception as e: + log.result_e(e) + log.unindent() + return False + log.result() # cleanup() - self.__reportTestStep("call cleanup()") + log.test("call cleanup()") try: _plugin.cleanup() except Exception as e: - self.__reportTestResult(e) + log.result_e(e) + log.unindent() return False - self.__reportTestResult() + log.result() + log.unindent() return True diff --git a/tests/streamcapture.py b/tests/streamcapture.py new file mode 100644 index 0000000..46d2c27 --- /dev/null +++ b/tests/streamcapture.py @@ -0,0 +1,52 @@ +# +# Copyright (C) 2008, 2009 Red Hat, Inc. +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program 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 this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# + +import sys, StringIO + +class StreamCapture: + + __capture = None + __stdout = None + + def __init__(self): + self.__stdout = sys.stdout + self.__capture = StringIO.StringIO() + + def capture(self): + sys.stdout = self.__capture + + def stdout(self): + sys.stdout = self.__stdout + + def getcaptured(self): + return self.__capture.getvalue() + + def clean(self): + self.__capture.close() + self.__capture = StringIO.StringIO() + + def close(self): + self.__capture.close() + + def __del__(self): + try: sys.stdout = self.__stdout + except: pass + try: self.__capture.close() + except: pass + +capture = StreamCapture() diff --git a/tests/tuned-test.py b/tests/tuned-test.py index 4e65d7c..601a099 100755 --- a/tests/tuned-test.py +++ b/tests/tuned-test.py @@ -17,12 +17,19 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # -import sys -import pluginstest +import sys, os +import pluginstest, admtest +from logging import log -# importing modules - parent directory -tunedir = sys.path[0] + "/.." +# importing modules from parent directory +tunedir = os.path.normpath(os.path.dirname(os.path.abspath(__file__)) + "/..") sys.path.insert(1, tunedir); +log.section("monitoring and tuning plugins") pt = pluginstest.PluginsTester(tunedir) pt.run() + +log.section("tuned-adm tests") +at = admtest.AdmTester() +at.run() +