2
0
mirror of https://github.com/xcat2/xcat-core.git synced 2026-10-07 18:16:34 +00:00
Files
xcat-core/xCAT-test/provtest/tests/test_config.py
T
Daniel Hilst 86533527e0 test(provtest): unit-test the offline half, and the rules it relies on
154 tests, standard library only, no network and no root: the config parser,
the assertion language, $step.field resolution, the offline machine, TAP
output, address arithmetic, and the decoding of captured dig, curl and tftp
output -- including an NXDOMAIN whose only record is an SOA, and a TFTP fetch
that created an empty file before finding the server had nothing to send.

test_boundaries.py asserts rules about this code rather than about xCAT: only
proc.py starts a process, no module opens a database or names an xCAT command,
and no step type can run one. A suite that asked xCAT what to expect would
pass on a cluster that could not boot a node.
2026-09-11 08:04:31 -03:00

207 lines
7.8 KiB
Python

"""Reading .conf files: sections, defaults, variables, and what is rejected.
Every scenario in this suite is data, so a mistake in a file has to fail loudly
at load time. Silence would mean a step that never ran counted as a pass.
"""
import os
import shutil
import tempfile
import unittest
import context # noqa: F401 (sys.path)
from provtest_lib import config
from provtest_lib.errors import ConfigError
BASIC = """
[vars]
domain = provtest.cluster
[defaults]
server = 10.99.1.1
timeout = 8
[scenario node-reverse]
description = The node's address resolves to its name
[step ptr]
type = dns
name = 11.1.99.10.in-addr.arpa
rrtype = PTR
assert =
status == NOERROR
data contains provtestcn.%(domain)s
"""
class Harness(unittest.TestCase):
def setUp(self):
self.dir = tempfile.mkdtemp(prefix="provtest-config-")
self.addCleanup(shutil.rmtree, self.dir, True)
def write(self, text, name="case.conf"):
path = os.path.join(self.dir, name)
with open(path, "w") as handle:
handle.write(text)
return path
def load(self, text, overrides=None, raw=False, name="case.conf"):
return config.load([self.write(text, name)], overrides or {}, raw)
class StructureTests(Harness):
def test_a_file_loads_into_scenarios_and_steps(self):
scenarios, variables = self.load(BASIC)
self.assertEqual(len(scenarios), 1)
scenario = scenarios[0]
self.assertEqual(scenario.name, "node-reverse")
self.assertEqual([step.name for step in scenario.steps], ["ptr"])
self.assertEqual(variables["domain"], "provtest.cluster")
def test_defaults_reach_a_step_that_does_not_set_them(self):
scenario = self.load(BASIC)[0][0]
self.assertEqual(scenario.steps[0].param("server"), "10.99.1.1")
self.assertEqual(scenario.steps[0].timeout, 8.0)
def test_a_step_overrides_a_default(self):
text = BASIC.replace("rrtype = PTR", "rrtype = PTR\nserver = 10.99.1.9")
scenario = self.load(text)[0][0]
self.assertEqual(scenario.steps[0].param("server"), "10.99.1.9")
def test_a_default_a_step_type_cannot_use_is_dropped_quietly(self):
# One [defaults] serves dns, tftp and http steps in the same file, so
# `path =` there must not make the dns step illegal.
text = BASIC.replace("timeout = 8", "timeout = 8\npath = /install/x")
scenario = self.load(text)[0][0]
self.assertIsNone(scenario.steps[0].param("path"))
def test_steps_keep_their_file_order(self):
text = BASIC + "\n[step second]\ntype = noop\n\n[step third]\ntype = noop\n"
scenario = self.load(text)[0][0]
self.assertEqual([step.name for step in scenario.steps],
["ptr", "second", "third"])
def test_assertions_are_parsed_into_the_step(self):
scenario = self.load(BASIC)[0][0]
targets = [item.target for item in scenario.steps[0].assertions]
self.assertEqual(targets, ["status", "data"])
class VariableTests(Harness):
def test_a_variable_is_interpolated_from_vars(self):
scenario = self.load(BASIC)[0][0]
value = scenario.steps[0].assertions[1].value
self.assertEqual(value, "provtestcn.provtest.cluster")
def test_an_override_beats_vars(self):
scenario = self.load(BASIC, {"domain": "other.cluster"})[0][0]
self.assertEqual(scenario.steps[0].assertions[1].value,
"provtestcn.other.cluster")
def test_an_undefined_variable_names_the_set_that_would_fix_it(self):
text = BASIC.replace("%(domain)s", "%(nosuchvar)s")
try:
self.load(text)
except ConfigError as exc:
self.assertIn("--set nosuchvar=", str(exc))
else:
self.fail("an undefined variable was accepted")
def test_raw_leaves_variables_alone(self):
# `validate` runs in a checkout where no value has been supplied yet.
scenario = self.load(BASIC, raw=True)[0][0]
self.assertIn("%(domain)s", scenario.steps[0].assertions[1].value)
def test_a_variable_never_becomes_a_step_key(self):
# --set path=/install must not silently become every step's path=.
scenario = self.load(BASIC, {"path": "/install"})[0][0]
self.assertIsNone(scenario.steps[0].param("path"))
def test_a_dollar_reference_survives_untouched(self):
text = BASIC + "\n[step after]\ntype = extract\nfrom = $ptr.data\npattern = (.*)\n"
scenario = self.load(text)[0][0]
self.assertEqual(scenario.steps[1].param("from"), "$ptr.data")
class RejectionTests(Harness):
def test_an_unknown_step_key_is_rejected(self):
text = BASIC.replace("rrtype = PTR", "rrtype = PTR\nnosuchkey = 1")
self.assertRaises(ConfigError, self.load, text)
def test_an_unknown_step_type_is_rejected(self):
self.assertRaises(ConfigError, self.load,
BASIC.replace("type = dns", "type = smtp"))
def test_a_step_with_no_type_is_rejected(self):
self.assertRaises(ConfigError, self.load,
BASIC.replace("type = dns\n", ""))
def test_a_step_before_any_scenario_is_rejected(self):
self.assertRaises(ConfigError, self.load,
"[step orphan]\ntype = noop\n")
def test_a_scenario_with_no_steps_is_rejected(self):
self.assertRaises(ConfigError, self.load,
"[scenario empty]\ndescription = nothing\n")
def test_an_unknown_section_is_rejected(self):
self.assertRaises(ConfigError, self.load, BASIC + "\n[nonsense]\nx = 1\n")
def test_type_may_not_be_a_default(self):
# A default type would turn a misspelt type= into another protocol.
self.assertRaises(ConfigError, self.load,
BASIC.replace("[defaults]", "[defaults]\ntype = dns"))
def test_a_repeated_step_name_in_one_file_is_rejected(self):
# configparser's own rule, and the reason step names are unique per
# file rather than per scenario.
self.assertRaises(ConfigError, self.load,
BASIC + "\n[scenario other]\n\n[step ptr]\ntype = noop\n")
def test_a_scenario_defined_in_two_files_is_rejected(self):
first = self.write(BASIC, "first.conf")
second = self.write(BASIC, "second.conf")
self.assertRaises(ConfigError, config.load, [first, second], {}, True)
def test_a_missing_file_says_so(self):
self.assertRaises(ConfigError, config.load,
[os.path.join(self.dir, "absent.conf")], {}, True)
def test_a_non_numeric_timeout_is_rejected(self):
self.assertRaises(ConfigError, self.load,
BASIC.replace("timeout = 8", "timeout = soon"))
class ShippedFileTests(unittest.TestCase):
"""The files this suite ships must load and validate as they stand."""
def test_every_shipped_file_parses(self):
from provtest_lib import machine
paths = context.conf_files()
self.assertTrue(paths, "no .conf files are shipped")
scenarios, _ = config.load(paths, {}, raw=True)
problems = []
for scenario in scenarios:
problems.extend(machine.validate_scenario(scenario))
self.assertEqual(problems, [])
def test_every_shipped_scenario_has_a_description_and_an_assertion(self):
scenarios, _ = config.load(context.conf_files(), {}, raw=True)
for scenario in scenarios:
self.assertTrue(scenario.description,
"%s has no description" % (scenario.name,))
asserted = any(step.assertions for step in scenario.steps)
self.assertTrue(asserted,
"%s asserts nothing at all" % (scenario.name,))
if __name__ == "__main__":
unittest.main()