From 95c8be553fcf4e64bcc643378e82fea5ea22c56d Mon Sep 17 00:00:00 2001 From: Pim van Pelt Date: Sun, 13 Mar 2022 17:20:10 +0000 Subject: [PATCH] Simple unit tester, with a few example tests --- tests.py | 106 ++++++++++++++++++++++++++++ unittest/correct-example1.yaml | 70 ++++++++++++++++++ unittest/error-interface-field.yaml | 10 +++ unittest/error-interface-mtu.yaml | 12 ++++ 4 files changed, 198 insertions(+) create mode 100755 tests.py create mode 100644 unittest/correct-example1.yaml create mode 100644 unittest/error-interface-field.yaml create mode 100644 unittest/error-interface-mtu.yaml diff --git a/tests.py b/tests.py new file mode 100755 index 0000000..2d9fc05 --- /dev/null +++ b/tests.py @@ -0,0 +1,106 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import sys +import yaml +import logging +from validator import Validator +import glob +import re + +try: + import argparse +except ImportError: + print("ERROR: install argparse manually: sudo pip install argparse") + sys.exit(-2) + + +def load_unittest(fn): + """ Read a two-document YAML file from 'fn', and expect the first document + to be a unittest specification, and the second file to be a config file to + be tested. Return them as a tuple, or [None, None] on error. """ + unittest = None + cfg = None + try: + with open(fn, "r") as f: + logging.debug("Reading test from %s" % fn) + n=0 + for data in yaml.load_all(f, Loader=yaml.Loader): + if n==0: + unittest = data + n = n + 1 + elif n==1: + cfg = data + n = n + 1 + else: + logging.error("Too many documents in %s" % fn) + return None, None + except: + logging.error("Couldn't read config from %s" % fn) + return None, None + return unittest, cfg + + +def main(): + parser = argparse.ArgumentParser(formatter_class=argparse.RawTextHelpFormatter) + parser.add_argument('-t', '--test', dest='test', type=str, nargs='+', default=['unittest/*.yaml'], help="""YAML test file(s)""") + parser.add_argument('-s', '--schema', dest='schema', type=str, default='./schema.yaml', help="""YAML schema validation file""") + parser.add_argument('-d', '--debug', dest='debug', action='store_true', help="""Enable debug, default False""") + + args = parser.parse_args() + level = logging.INFO + if args.debug: + level = logging.DEBUG + logging.basicConfig(format='[%(levelname)-8s] %(name)s.%(funcName)s: %(message)s', level=level) + logging.debug("Arguments: %s" % args) + + errors = 0 + tests = 0 + for pattern in args.test: + for fn in glob.glob(pattern): + tests = tests + 1 + unittest, cfg = load_unittest(fn) + if not unittest or not cfg: + errors = errors + 1 + continue + + logging.info("Unittest %s" % fn) + logging.debug("Unittest: %s" % unittest) + logging.debug("Config: %s" % cfg) + this_failed =False + v = Validator(schema=args.schema) + rv, msgs = v.validate(cfg) + try: + if len(msgs) != unittest['test']['errors']['count']: + logging.error("Unittest %s failed: expected %d error messages, got %d" % (fn, unittest['test']['errors']['count'], len(msgs))) + this_failed = True + except: + pass + + msgs_unexpected = 0 + msgs_expected = [] + if 'test' in unittest and 'errors' in unittest['test'] and 'expected' in unittest['test']['errors']: + msgs_expected = unittest['test']['errors']['expected'] + + for m in msgs: + this_msg_expected = False + for expected in msgs_expected: + logging.debug("Checking expected '%s'" % expected) + if re.search(expected, m): + logging.debug("Expected msg '%s' based on regexp '%s'" % (m, expected)) + this_msg_expected = True + break + if not this_msg_expected: + logging.error("Unexpected message: %s" % (m)) + this_failed = True + if this_failed: + logging.error("Unittest %s failed" % (fn)) + errors = errors + 1 + else: + logging.info("Unittest %s passed" % (fn)) + + logging.info("Tests: %d run, %d failed" % (tests, errors)) + + +if __name__ == "__main__": + main() diff --git a/unittest/correct-example1.yaml b/unittest/correct-example1.yaml new file mode 100644 index 0000000..7e8c9a7 --- /dev/null +++ b/unittest/correct-example1.yaml @@ -0,0 +1,70 @@ +test: + description: "Test that is meant to pass" + errors: + count: 0 +--- +bondethernets: + BondEthernet0: + description: "Infra: xsw0.lab.ipng.ch LACP" + interfaces: [ GigabitEthernet2/0/0, GigabitEthernet2/0/1 ] + +interfaces: + GigabitEthernet1/0/0: + description: "Infra: nikhef-core-1.nl.switch.coloclue.net e1/34" + lcp: e0-0 + addresses: [ 94.142.244.85/24, 2A02:898::146:1/64 ] + sub-interfaces: + 100: + description: "Cust: hvn0.nlams0.ipng.ch" + addresses: [ 94.142.241.185/29, 2a02:898:146::1/64 ] + 101: + description: "Infra: L2 for FrysIX AS112" + + GigabitEthernet1/0/1: + description: "Broken - has same LCP as above" + lcp: e0-1 + + GigabitEthernet2/0/0: + description: "Infra: LAG to xsw0" + + GigabitEthernet2/0/1: + description: "Infra: LAG to xsw1" + + GigabitEthernet3/0/0: + description: "Infra: Bridge Doamin 10" + + BondEthernet0: + description: "Bond, James Bond!" + mac: 00:01:02:03:04:05 + lcp: "bond0" + sub-interfaces: + 200: + encapsulation: + dot1q: 1000 + 201: + encapsulation: + dot1ad: 1000 + 202: + encapsulation: + dot1q: 1000 + inner-dot1q: 1234 + addresses: [ 192.0.2.1/24 ] + 203: + encapsulation: + dot1ad: 1000 + inner-dot1q: 1000 + +loopbacks: + loop0: + description: "Core: example.ipng.ch" + mtu: 9216 + lcp: "loop0" + addresses: [ 192.0.2.1/32, 2001:db8:1::1/128 ] + +bridgedomains: + bd10: + description: "Bridge Domain 10" + mtu: 1500 + lcp: "bvi10" + addresses: [ 192.0.2.9/29, 2001:db8:2::1/64 ] + interfaces: [ BondEthernet0.203, GigabitEthernet3/0/0 ] diff --git a/unittest/error-interface-field.yaml b/unittest/error-interface-field.yaml new file mode 100644 index 0000000..c54ba72 --- /dev/null +++ b/unittest/error-interface-field.yaml @@ -0,0 +1,10 @@ +test: + description: "Interface description field mistyped" + errors: + expected: + - "descr.*Unexpected element" + count: 1 +--- +interfaces: + GigabitEthernet1/0/0: + descr: "some description" diff --git a/unittest/error-interface-mtu.yaml b/unittest/error-interface-mtu.yaml new file mode 100644 index 0000000..973f755 --- /dev/null +++ b/unittest/error-interface-mtu.yaml @@ -0,0 +1,12 @@ +test: + description: "MTU too small on one interface, too large on another" + errors: + expected: + - "mtu.*less than 128" + - "mtu.*greater than 9216" +--- +interfaces: + GigabitEthernet1/0/0: + mtu: 9217 + GigabitEthernet1/0/1: + mtu: 127