Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions framework/python/src/common/device.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,9 @@ def get_reports(self):

# TODO: Add ability to remove reports once test reports have been cleaned up

def to_json(self):
def to_dict(self):
"""Returns the device as a python dictionary. This is used for the
# system status API endpoint and in the report."""
system status API endpoint and in the report."""
device_json = {}
device_json['mac_addr'] = self.mac_addr
device_json['manufacturer'] = self.manufacturer
Expand Down
8 changes: 5 additions & 3 deletions framework/python/src/test_orc/module.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,21 @@
# See the License for the specific language governing permissions and
# limitations under the License.

"""Represemts a test module."""
from dataclasses import dataclass
"""Represents a test module."""
from dataclasses import dataclass, field
from docker.models.containers import Container


@dataclass
class TestModule: # pylint: disable=too-few-public-methods,too-many-instance-attributes
"""Represents a test module."""

# General test module information
name: str = None
display_name: str = None
description: str = None
tests: list = field(default_factory=lambda: [])

# Docker settings
build_file: str = None
container: Container = None
container_name: str = None
Expand Down
26 changes: 26 additions & 0 deletions framework/python/src/test_orc/test_case.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Copyright 2023 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Represents an individual test case."""
from dataclasses import dataclass


@dataclass
class TestCase: # pylint: disable=too-few-public-methods,too-many-instance-attributes
"""Represents a test case."""

name: str = "test.undefined"
description: str = ""
expected_behavior: str = ""
required_result: str = "Recommended"
52 changes: 49 additions & 3 deletions framework/python/src/test_orc/test_orchestrator.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
from common import logger, util
from common.testreport import TestReport
from test_orc.module import TestModule
from test_orc.test_case import TestCase

LOG_NAME = "test_orc"
LOGGER = logger.get_logger("test_orc")
Expand Down Expand Up @@ -97,12 +98,12 @@ def run_test_modules(self):
def _generate_report(self):

report = {}
report["device"] = self._session.get_target_device().to_json()
report["device"] = self._session.get_target_device().to_dict()
report["started"] = self._session.get_started().strftime(
"%Y-%m-%d %H:%M:%S")
report["finished"] = self._session.get_finished().strftime(
"%Y-%m-%d %H:%M:%S")
report["status"] = self._session.get_status()
report["status"] = self._calculate_result()
report["tests"] = self._session.get_report_tests()
out_file = os.path.join(
self._root_path, RUNTIME_DIR,
Expand All @@ -114,6 +115,15 @@ def _generate_report(self):
util.run_command(f"chown -R {self._host_user} {out_file}")
return report

def _calculate_result(self):
result = "Compliant"
for test_result in self._session.get_test_results():
test_case = self.get_test_case(test_result["name"])
if (test_case.required_result.lower() == "required"
and test_result["result"].lower() == "non-compliant"):
result = "non-compliant"
return result

def _cleanup_old_test_results(self, device):

if device.max_device_reports is not None:
Expand Down Expand Up @@ -340,7 +350,7 @@ def _load_test_modules(self):
def _load_test_module(self, module_dir):
"""Import module configuration from module_config.json."""

LOGGER.debug("Loading test module " + module_dir)
LOGGER.debug(f"Loading test module {module_dir}")

modules_dir = os.path.join(self._path, TEST_MODULES_DIR)

Expand All @@ -359,8 +369,21 @@ def _load_test_module(self, module_dir):
module.container_name = "tr-ct-" + module.dir_name + "-test"
module.image_name = "test-run/" + module.dir_name + "-test"

# Load test cases
if "tests" in module_json["config"]:
module.total_tests = len(module_json["config"]["tests"])
for test_case_json in module_json["config"]["tests"]:
try:
test_case = TestCase(
name=test_case_json["name"],
description=test_case_json["description"],
expected_behavior=test_case_json["expected_behavior"],
required_result=test_case_json["required_result"]
)
module.tests.append(test_case)
except Exception as error:
LOGGER.debug("Failed to load test case. See error for details")
LOGGER.error(error)

if "timeout" in module_json["config"]["docker"]:
module.timeout = module_json["config"]["docker"]["timeout"]
Expand All @@ -374,6 +397,7 @@ def _load_test_module(self, module_dir):
if "network" in module_json["config"]:
module.network = module_json["config"]["network"]

# Ensure container is built after any dependencies
if "depends_on" in module_json["config"]["docker"]:
depends_on_module = module_json["config"]["docker"]["depends_on"]
if self._get_test_module(depends_on_module) is None:
Expand Down Expand Up @@ -424,3 +448,25 @@ def _stop_module(self, module, kill=False):
LOGGER.debug("Container stopped:" + module.container_name)
except docker.errors.NotFound:
pass

def get_test_modules(self):
return self._test_modules

def get_test_module(self, name):
for test_module in self.get_test_modules():
if test_module.name == name:
return test_module
return None

def get_test_cases(self):
test_cases = []
for test_module in self.get_test_modules():
for test_case in test_module.tests:
test_cases.append(test_case)
return test_cases

def get_test_case(self, name):
for test_case in self.get_test_cases():
if test_case.name == name:
return test_case
return None
6 changes: 3 additions & 3 deletions modules/test/baseline/conf/module_config.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,19 +16,19 @@
"name": "baseline.pass",
"description": "Simulate a compliant test",
"expected_behavior": "A compliant test result is generated",
"short_description": "A compliant test result is generated"
"required_result": "Required"
},
{
"name": "baseline.fail",
"description": "Simulate a non-compliant test",
"expected_behavior": "A non-compliant test result is generated",
"short_description": "A non-compliant test result is generated"
"required_result": "Recommended"
},
{
"name": "baseline.skip",
"description": "Simulate a skipped test",
"expected_behavior": "A skipped test result is generated",
"short_description": "A skipped test result is generated"
"required_result": "Roadmap"
}
]
}
Expand Down
27 changes: 14 additions & 13 deletions modules/test/conn/conf/module_config.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,37 +17,37 @@
"name": "connection.dhcp.disconnect",
"description": "The device under test has received an IP address from the DHCP server and responds to an ICMP echo (ping) request",
"expected_behavior": "The device is not setup with a static IP address. The device accepts an IP address from a DHCP server (RFC 2131) and responds succesfully to an ICMP echo (ping) request.",
"short_description": "Device has received an IP address after port disconnect"
"required_result": "Required"
},
{
"name": "connection.dhcp.disconnect_ip_change",
"description": "Update device IP on the DHCP server and reconnect the device. Does the device receive the new IP address?",
"expected_behavior": "Device recieves a new IP address within the range that is specified on the DHCP server. Device should respond to aping on this new address.",
"short_description": "Device has received new IP address after port disconnect"
"required_result": "Required"
},
{
"name": "connection.dhcp_address",
"description": "The device under test has received an IP address from the DHCP server and responds to an ICMP echo (ping) request",
"expected_behavior": "The device is not setup with a static IP address. The device accepts an IP address from a DHCP server (RFC 2131) and responds succesfully to an ICMP echo (ping) request.",
"short_description": "Device has received a DHCP provided IP address"
"required_result": "Required"
},
{
"name": "connection.mac_address",
"description": "Check and note device physical address.",
"expected_behavior": "N/A",
"short_description": "Device MAC address resolved"
"required_result": "Required"
},
{
"name": "connection.mac_oui",
"description": "The device under test hs a MAC address prefix that is registered against a known manufacturer.",
"expected_behavior": "The MAC address prefix is registered in the IEEE Organizationally Unique Identifier database.",
"short_description": "OUI for MAC address resolved"
"required_result": "Required"
},
{
"name": "connection.private_address",
"description": "The device under test accepts an IP address that is compliant with RFC 1918 Address Allocation for Private Internets.",
"expected_behavior": "The device under test accepts IP addresses within all ranges specified in RFC 1918 and communicates using these addresses. The Internet Assigned Numbers Authority (IANA) has reserved the following three blocks of the IP address space for private internets. 10.0.0.0 - 10.255.255.255.255 (10/8 prefix). 172.16.0.0 - 172.31.255.255 (172.16/12 prefix). 192.168.0.0 - 192.168.255.255 (192.168/16 prefix)",
"short_description": "Device supports private addresses",
"required_result": "Required",
"config": {
"ranges": [
{
Expand All @@ -69,7 +69,7 @@
"name": "connection.shared_address",
"description": "Ensure the device supports RFC 6598 IANA-Reserved IPv4 Prefix for Shared Address Space",
"expected_behavior": "The device under test accepts IP addresses within the ranges specified in RFC 6598 and communicates using these addresses",
"short_description": "Device supports shared address space",
"required_result": "Required",
"config": {
"ranges": [
{
Expand All @@ -83,6 +83,7 @@
"name": "connection.private_address",
"description": "The device under test accepts an IP address that is compliant with RFC 1918 Address Allocation for Private Internets.",
"expected_behavior": "The device under test accepts IP addresses within all ranges specified in RFC 1918 and communicates using these addresses. The Internet Assigned Numbers Authority (IANA) has reserved the following three blocks of the IP address space for private internets. 10.0.0.0 - 10.255.255.255.255 (10/8 prefix). 172.16.0.0 - 172.31.255.255 (172.16/12 prefix). 192.168.0.0 - 192.168.255.255 (192.168/16 prefix)",
"required_result": "Required",
"config": [
{
"start": "10.0.0.100",
Expand All @@ -102,37 +103,37 @@
"name": "connection.single_ip",
"description": "The network switch port connected to the device reports only one IP address for the device under test.",
"expected_behavior": "The device under test does not behave as a network switch and only requets one IP address. This test is to avoid that devices implement network switches that allow connecting strings of daisy chained devices to one single network port, as this would not make 802.1x port based authentication possible.",
"short_description": "Device only reports one IP address"
"required_result": "Required"
},
{
"name": "connection.target_ping",
"description": "The device under test responds to an ICMP echo (ping) request.",
"expected_behavior": "The device under test responds to an ICMP echo (ping) request.",
"short_description": "Device responds to a ping request"
"required_result": "Required"
},
{
"name": "connection.ipaddr.ip_change",
"description": "The device responds to a ping (ICMP echo request) to the new IP address it has received after the initial DHCP lease has expired.",
"expected_behavior": "If the lease expires before the client receiveds a DHCPACK, the client moves to INIT state, MUST immediately stop any other network processing and requires network initialization parameters as if the client were uninitialized. If the client then receives a DHCPACK allocating the client its previous network addres, the client SHOULD continue network processing. If the client is given a new network address, it MUST NOT continue using the previous network address and SHOULD notify the local users of the problem.",
"short_description": "Device receives an IP change from the DHCP server"
"required_result": "Required"
},
{
"name": "connection.ipaddr.dhcp_failover",
"description": "The device has requested a DHCPREQUEST/REBIND to the DHCP failover server after the primary DHCP server has been brought down.",
"expected_behavior": "",
"short_description": "Device receives IP address from primary and failover DHCP servers"
"required_result": "Required"
},
{
"name": "connection.ipv6_slaac",
"description": "The device forms a valid IPv6 address as a combination of the IPv6 router prefix and the device interface identifier",
"expected_behavior": "The device under test complies with RFC4862 and forms a valid IPv6 SLAAC address",
"short_description": "Device uses an IPv6 address using SLAAC"
"required_result": "Required"
},
{
"name": "connection.ipv6_ping",
"description": "The device responds to an IPv6 ping (ICMPv6 Echo) request to the SLAAC address",
"expected_behavior": "The device responds to the ping as per RFC4443",
"short_description": "Device responds to an IPv6 SLAAC address ping request"
"required_result": "Required"
}
]
}
Expand Down
9 changes: 5 additions & 4 deletions modules/test/dns/conf/module_config.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,21 +13,22 @@
},
"tests":[
{
"name": "dns.network.from_device",
"name": "dns.network.hostname_resolution",
"description": "Verify the device sends DNS requests",
"expected_behavior": "The device sends DNS requests.",
"short_description": "The device sends DNS requests."
"required_result": "Required"
},
{
"name": "dns.network.from_dhcp",
"description": "Verify the device allows for a DNS server to be entered automatically",
"expected_behavior": "The device sends DNS requests to the DNS server provided by the DHCP server",
"short_description": "The device sends DNS requests to local DNS server."
"required_result": "Roadmap"
},
{
"name": "dns.mdns",
"description": "If the device has MDNS (or any kind of IP multicast), can it be disabled",
"short_description": "MDNS traffic detected from device"
"expected_behavior": "Device may send MDNS requests",
"required_result": "Recommended"
}
]
}
Expand Down
Loading