Skip to content

Commit

Permalink
Support running test suites via a python script (#11996)
Browse files Browse the repository at this point in the history
* Start adding a file that is able to name possible tests

* Move test logic into separate python module, for easier code logic split

* Add support for linux netns in tests including a validation shell. tested that this works in host. Updated vscode container to also allow running these

* Add milliseconds for logging, add sleep for tentative ipv6 addresses

* Restyle fixes

* Prepare for test execution

* more work to make runners functional

* This can now run unit tests (at least in linux)

* Restyle fixes

* Add TV app as a supported application on host builds

* Finished reporting on error only

* Restyle and run new tests on linux using the new method

* Fix the test yaml file

* Remove run tv tests step - now all tests are run in one

* Remove todo - it is done

* Fix name of the subcommand

* Run tests on darwin as well using the updated python scripts

* Add glob  support for running tests (hackish implmementation though through symlinks) and avoid tv tests for darwin

* Add missing link file

* Fix target skip glob argument for darwin

* Minor updates from darwin testing: start thread after full constructor run, cleanup code a bit

* Add some locking th definition logging captures

* Use PTY since that removes buffering on darwin

* Only use pty on darwin for IPC

* Restyle fixes

* Update scripts/tests/chiptest/linux.py

Co-authored-by: Victor Morales <[email protected]>

* Update scripts/tests/chiptest/runner.py

Co-authored-by: Victor Morales <[email protected]>

* Restyle fixes

* Run 2 iterations  of test suites to preserve previous behaviour, remove test_suites.sh

* Add extra validation: if chip-all-clusters-app fails, make the test runner fail the test

Co-authored-by: Victor Morales <[email protected]>
  • Loading branch information
andy31415 and electrocucaracha authored Nov 25, 2021
1 parent 23fad89 commit 13de723
Show file tree
Hide file tree
Showing 13 changed files with 678 additions and 280 deletions.
1 change: 1 addition & 0 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"--security-opt",
"seccomp=unconfined",
"--network=host",
"--privileged",
"-v",
"/dev/bus/usb:/dev/bus/usb:ro",
"--device-cgroup-rule=a 189:* rmw",
Expand Down
35 changes: 19 additions & 16 deletions .github/workflows/tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -84,13 +84,15 @@ jobs:
# actually succeeded, because that just wastes space.
rsync -a out/debug/standalone/ objdir-clone || true
- name: Run Tests
timeout-minutes: 20
timeout-minutes: 30
run: |
scripts/tests/test_suites.sh -n
- name: Run TV Tests
timeout-minutes: 10
run: |
scripts/tests/test_suites.sh -n -a tv
./scripts/run_in_build_env.sh \
"./scripts/tests/run_test_suite.py run \
--iterations 2 \
--chip-tool ./out/debug/standalone/chip-tool \
--all-clusters-app ./out/debug/standalone/chip-all-clusters-app \
--tv-app ./out/debug/standalone/chip-tv-app \
"
- name: Uploading core files
uses: actions/upload-artifact@v2
if: ${{ failure() }} && ${{ !env.ACT }}
Expand Down Expand Up @@ -167,17 +169,18 @@ jobs:
# The idea is to not upload our objdir unless builds have
# actually succeeded, because that just wastes space.
rsync -a out/debug/standalone/ objdir-clone || true
- name: Run Test Suites
timeout-minutes: 35
- name: Run Tests
timeout-minutes: 45
run: |
scripts/tests/test_suites.sh
- name: Uploading application logs
uses: actions/upload-artifact@v2
if: ${{ failure() }} && ${{ !env.ACT }}
with:
name: test-suite-app-logs-${{ matrix.type }}-${{ matrix.eventloop }}
path: /tmp/test_suites_app_logs/
retention-days: 5
./scripts/run_in_build_env.sh \
"./scripts/tests/run_test_suite.py \
--target-skip-glob 'tv-*' \
run \
--iterations 2 \
--chip-tool ./out/debug/standalone/chip-tool \
--all-clusters-app ./out/debug/standalone/chip-all-clusters-app \
--tv-app ./out/debug/standalone/chip-tv-app \
"
- name: Uploading core files
uses: actions/upload-artifact@v2
if: ${{ failure() }} && ${{ !env.ACT }}
Expand Down
1 change: 0 additions & 1 deletion .restyled.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,6 @@ exclude:
- "third_party/nanopb/repo/**/*"
- "src/android/CHIPTool/gradlew" # gradle wrapper generated file
- "third_party/android_deps/gradlew" # gradle wrapper generated file
- "scripts/tests/test_suites.sh" # overly agressive shell harden


changed_paths:
Expand Down
4 changes: 3 additions & 1 deletion scripts/build/build/targets.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,9 +106,11 @@ def HostTargets():

app_targets = []

# RPC console compilation only for native
# Don't cross compile some builds
app_targets.append(
targets[0].Extend('rpc-console', app=HostApp.RPC_CONSOLE))
app_targets.append(
targets[0].Extend('tv-app', app=HostApp.TV_APP))

for target in targets:
app_targets.append(target.Extend(
Expand Down
6 changes: 6 additions & 0 deletions scripts/build/builders/host.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ class HostApp(Enum):
THERMOSTAT = auto()
RPC_CONSOLE = auto()
MIN_MDNS = auto()
TV_APP = auto()

def ExamplePath(self):
if self == HostApp.ALL_CLUSTERS:
Expand All @@ -38,6 +39,8 @@ def ExamplePath(self):
return 'common/pigweed/rpc_console'
if self == HostApp.MIN_MDNS:
return 'minimal-mdns'
if self == HostApp.TV_APP:
return 'tv-app/linux'
else:
raise Exception('Unknown app type: %r' % self)

Expand All @@ -60,6 +63,9 @@ def OutputNames(self):
yield 'minimal-mdns-client.map'
yield 'minimal-mdns-server'
yield 'minimal-mdns-server.map'
elif self == HostApp.TV_APP:
yield 'chip-tv-app'
yield 'chip-tv-app.map'
else:
raise Exception('Unknown app type: %r' % self)

Expand Down
12 changes: 12 additions & 0 deletions scripts/build/testdata/build_linux_on_x64.txt
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,12 @@ gn gen --check --fail-on-unused-args --root={root}/examples/thermostat/linux {ou
# Generating linux-x64-thermostat-ipv6only
gn gen --check --fail-on-unused-args --root={root}/examples/thermostat/linux --args=chip_inet_config_enable_ipv4=false {out}/linux-x64-thermostat-ipv6only

# Generating linux-x64-tv-app
gn gen --check --fail-on-unused-args --root={root}/examples/tv-app/linux {out}/linux-x64-tv-app

# Generating linux-x64-tv-app-ipv6only
gn gen --check --fail-on-unused-args --root={root}/examples/tv-app/linux --args=chip_inet_config_enable_ipv4=false {out}/linux-x64-tv-app-ipv6only

# Building linux-arm64-all-clusters
ninja -C {out}/linux-arm64-all-clusters

Expand Down Expand Up @@ -118,3 +124,9 @@ ninja -C {out}/linux-x64-thermostat

# Building linux-x64-thermostat-ipv6only
ninja -C {out}/linux-x64-thermostat-ipv6only

# Building linux-x64-tv-app
ninja -C {out}/linux-x64-tv-app

# Building linux-x64-tv-app-ipv6only
ninja -C {out}/linux-x64-tv-app-ipv6only
52 changes: 52 additions & 0 deletions scripts/tests/chiptest/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
#
# Copyright (c) 2021 Project CHIP Authors
#
# 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
#
# http://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.
#

from pathlib import Path
import os
import logging

import chiptest.linux
import chiptest.runner

from .test_definition import TestTarget, TestDefinition, ApplicationPaths


def AllTests(root: str):
"""Gets all the tests that can be found in the ROOT directory based on
yaml file names.
"""
for path in Path(os.path.join(root, 'src', 'app', 'tests', 'suites')).rglob("*.yaml"):
logging.debug('Found YAML: %s' % path)

# grab the name without the extension
name = path.stem.lower()

if 'simulated' in name:
continue

if name.startswith('tv_'):
target = TestTarget.TV
name = 'tv-' + name[3:]
elif name.startswith('test_'):
target = TestTarget.ALL_CLUSTERS
name = 'app-' + name[5:]
else:
continue

yield TestDefinition(yaml_file=path, run_name=path.stem, name=name, target=target)


__all__ = ['TestTarget', 'TestDefinition', 'AllTests', 'ApplicationPaths']
1 change: 1 addition & 0 deletions scripts/tests/chiptest/glob_matcher.py
138 changes: 138 additions & 0 deletions scripts/tests/chiptest/linux.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
#
# Copyright (c) 2021 Project CHIP Authors
#
# 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
#
# http://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.
#

"""
Handles linux-specific functionality for running test cases
"""

import logging
import os
import subprocess
import sys
import time

from .test_definition import ApplicationPaths

test_environ = os.environ.copy()


def EnsureNetworkNamespaceAvailability():
if os.getuid() == 0:
logging.debug("Current user is root")
logging.warn("Running as root and this will change global namespaces.")
return

os.execvpe("unshare", ["unshare", "--map-root-user", "-n", "-m", "python3",
sys.argv[0], '--internal-inside-unshare'] + sys.argv[1:], test_environ)


def EnsurePrivateState():
logging.info("Ensuring /run is privately accessible")

logging.debug("Making / private")
if os.system("mount --make-private /") != 0:
logging.error("Failed to make / private")
logging.error("Are you using --privileged if running in docker?")
sys.exit(1)

logging.debug("Remounting /run")
if os.system("mount -t tmpfs tmpfs /run") != 0:
logging.error("Failed to mount /run as a temporary filesystem")
logging.error("Are you using --privileged if running in docker?")
sys.exit(1)


def CreateNamespacesForAppTest():
"""
Creates appropriate namespaces for a tool and app binaries in a simulated
isolated network.
"""
COMMANDS = [
# 2 virtual hosts: for app and for the tool
"ip netns add app",
"ip netns add tool",

# create links for switch to net connections
"ip link add eth-app type veth peer name eth-app-switch",
"ip link add eth-tool type veth peer name eth-tool-switch",

# link the connections together
"ip link set eth-app netns app",
"ip link set eth-tool netns tool",

"ip link add name br1 type bridge",
"ip link set br1 up",
"ip link set eth-app-switch master br1",
"ip link set eth-tool-switch master br1",

# mark connections up
"ip netns exec app ip addr add 10.10.10.1/24 dev eth-app",
"ip netns exec app ip link set dev eth-app up",
"ip netns exec app ip link set dev lo up",
"ip link set dev eth-app-switch up",

"ip netns exec tool ip addr add 10.10.10.2/24 dev eth-tool",
"ip netns exec tool ip link set dev eth-tool up",
"ip netns exec tool ip link set dev lo up",
"ip link set dev eth-tool-switch up",

# Force IPv6 to use ULAs that we control
"ip netns exec tool ip -6 addr flush eth-tool",
"ip netns exec app ip -6 addr flush eth-app",
"ip netns exec tool ip -6 a add fd00:0:1:1::2/64 dev eth-tool",
"ip netns exec app ip -6 a add fd00:0:1:1::3/64 dev eth-app",
]

for command in COMMANDS:
logging.debug("Executing '%s'" % command)
if os.system(command) != 0:
logging.error("Failed to execute '%s'" % command)
logging.error("Are you using --privileged if running in docker?")
sys.exit(1)

# IPv6 does Duplicate Address Detection even though
# we know ULAs provided are isolated. Wait for 'tenative' address to be gone

logging.info('Waiting for IPv6 DaD to complete (no tentative addresses)')
for i in range(100): # wait at most 10 seconds
output = subprocess.check_output(['ip', 'addr'])
if b'tentative' not in output:
logging.info('No more tentative addresses')
break
time.sleep(0.1)
else:
logging.warn("Some addresses look to still be tentative")


def PrepareNamespacesForTestExecution(in_unshare: bool):
if not in_unshare:
EnsureNetworkNamespaceAvailability()
elif in_unshare:
EnsurePrivateState()

CreateNamespacesForAppTest()


def PathsWithNetworkNamespaces(paths: ApplicationPaths) -> ApplicationPaths:
"""
Returns a copy of paths with updated command arrays to invoke the
commands in an appropriate network namespace.
"""
return ApplicationPaths(
chip_tool='ip netns exec tool'.split() + paths.chip_tool,
all_clusters_app='ip netns exec app'.split() + paths.all_clusters_app,
tv_app='ip netns exec app'.split() + paths.tv_app,
)
Loading

0 comments on commit 13de723

Please sign in to comment.