[PSM Interop] Add unittests CI with github actions (#34125)

- Add Github Action to conditionally run PSM Interop unit tests:
- Only run when changes are detected in
`tools/run_tests/xds_k8s_test_driver` or any of the proto files used by
the driver
  - Only run against PRs and pushes to `master`, `v1.*.*` branches
  - Runs using `python3.9` and `python3.10`
  - Ready to be added to the list of required GitHub checks
- Add `tools/run_tests/xds_k8s_test_driver/tests/unit/__main__.py` test
loader that recursively discovers all unit tests in
`tools/run_tests/xds_k8s_test_driver/tests/unit`
- Add basic coverage for `XdsTestClient` and `XdsTestServer` to verify
the test loader picks up all folders

Related:
- First unit tests without automated CI added in #34097
pull/34130/head^2
Sergii Tkachenko 2 years ago committed by GitHub
parent 440eef2288
commit 1708f631ee
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 74
      .github/workflows/psm-interop.yaml
  2. 10
      tools/run_tests/xds_k8s_test_driver/run.sh
  3. 27
      tools/run_tests/xds_k8s_test_driver/tests/unit/__main__.py
  4. 13
      tools/run_tests/xds_k8s_test_driver/tests/unit/test_app/__init__.py
  5. 55
      tools/run_tests/xds_k8s_test_driver/tests/unit/test_app/client_app_test.py
  6. 84
      tools/run_tests/xds_k8s_test_driver/tests/unit/test_app/server_app_test.py

@ -0,0 +1,74 @@
name: PSM Interop
on:
pull_request:
push:
branches:
- master
- 'v1.*'
permissions:
contents: read
jobs:
unittest:
# By default, only version is printed out in parens, e.g. "unittest (3.10)"
# This changes it to "unittest (python3.10)"
name: "unittest (python${{ matrix.python_version }})"
runs-on: ubuntu-latest
strategy:
matrix:
python_version: ["3.9", "3.10"]
fail-fast: false
permissions:
pull-requests: read # Used by paths-filter to read the diff.
defaults:
run:
working-directory: 'tools/run_tests/xds_k8s_test_driver'
steps:
- uses: actions/checkout@v3
# To add this job to required GitHub checks, it's not enough to use
# the on.pull_request.paths filter. For required checks, the job needs to
# return the success status, and not be skipped.
# Using paths-filter action, we skip the setup/test steps when psm interop
# files are unchanged, and the job returns success.
- uses: dorny/paths-filter@v2
id: paths_filter
with:
filters: |
psm_interop_src:
- 'tools/run_tests/xds_k8s_test_driver/**'
- 'src/proto/grpc/testing/empty.proto'
- 'src/proto/grpc/testing/messages.proto'
- 'src/proto/grpc/testing/test.proto'
- uses: actions/setup-python@v4
if: ${{ steps.paths_filter.outputs.psm_interop_src == 'true' }}
with:
python-version: "${{ matrix.python_version }}"
cache: 'pip'
cache-dependency-path: 'tools/run_tests/xds_k8s_test_driver/requirements.lock'
- name: "Install requirements"
if: ${{ steps.paths_filter.outputs.psm_interop_src == 'true' }}
run: |
pip list
pip install --upgrade pip setuptools
pip list
pip install -r requirements.lock
pip list
- name: "Generate protos"
if: ${{ steps.paths_filter.outputs.psm_interop_src == 'true' }}
run: >
python -m grpc_tools.protoc --proto_path=../../../
--python_out=. --grpc_python_out=.
src/proto/grpc/testing/empty.proto
src/proto/grpc/testing/messages.proto
src/proto/grpc/testing/test.proto
- name: "Run unit tests"
if: ${{ steps.paths_filter.outputs.psm_interop_src == 'true' }}
run: python -m tests.unit

@ -63,5 +63,11 @@ export PYTHONPATH="${XDS_K8S_DRIVER_DIR}"
# Split path to python file from the rest of the args.
readonly PY_FILE="$1"
shift
# Append args after --flagfile, so they take higher priority.
exec python "${PY_FILE}" --flagfile="${XDS_K8S_CONFIG}" "$@"
if [[ "${PY_FILE}" =~ tests/unit($|/) ]]; then
# Do not set the flagfile when running unit tests.
exec python "${PY_FILE}" "$@"
else
# Append args after --flagfile, so they take higher priority.
exec python "${PY_FILE}" --flagfile="${XDS_K8S_CONFIG}" "$@"
fi

@ -0,0 +1,27 @@
# Copyright 2023 The gRPC 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.
"""Discover and run all unit tests recursively."""
import pathlib
from absl.testing import absltest
def load_tests(loader: absltest.TestLoader, unused_tests, unused_pattern):
unit_tests_root = pathlib.Path(__file__).parent
return loader.discover(f"{unit_tests_root}", pattern="*_test.py")
if __name__ == "__main__":
absltest.main()

@ -0,0 +1,13 @@
# Copyright 2023 gRPC 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.

@ -0,0 +1,55 @@
# Copyright 2023 gRPC 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 typing import Optional
from absl.testing import absltest
from framework.test_app import client_app
# Alias
XdsTestClient = client_app.XdsTestClient
# Test values.
CANNED_IP: str = "10.0.0.42"
CANNED_RPC_PORT: int = 1111
CANNED_HOSTNAME: str = "test-client.local"
CANNED_SERVER_TARGET: str = "xds:///test-server"
class ClientAppTest(absltest.TestCase):
"""Unit test for the ClientApp."""
def test_constructor(self):
xds_client = XdsTestClient(
ip=CANNED_IP,
rpc_port=CANNED_RPC_PORT,
hostname=CANNED_HOSTNAME,
server_target=CANNED_SERVER_TARGET,
)
# Channels list empty.
self.assertEmpty(xds_client.channels)
# Test fields set as is.
self.assertEqual(xds_client.ip, CANNED_IP)
self.assertEqual(xds_client.rpc_port, CANNED_RPC_PORT)
self.assertEqual(xds_client.server_target, CANNED_SERVER_TARGET)
self.assertEqual(xds_client.hostname, CANNED_HOSTNAME)
# Test optional argument defaults.
self.assertEqual(xds_client.rpc_host, CANNED_IP)
self.assertEqual(xds_client.maintenance_port, CANNED_RPC_PORT)
if __name__ == "__main__":
absltest.main()

@ -0,0 +1,84 @@
# Copyright 2023 gRPC 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 typing import Optional
from absl.testing import absltest
from framework.test_app import server_app
# Alias
XdsTestServer = server_app.XdsTestServer
# Test values.
CANNED_IP: str = "10.0.0.43"
CANNED_RPC_PORT: int = 2222
CANNED_HOSTNAME: str = "test-server.local"
CANNED_XDS_HOST: str = "xds-test-server"
CANNED_XDS_PORT: int = 42
class ServerAppTest(absltest.TestCase):
"""Unit test for the XdsTestServer."""
def test_constructor(self):
xds_server = XdsTestServer(
ip=CANNED_IP,
rpc_port=CANNED_RPC_PORT,
hostname=CANNED_HOSTNAME,
)
# Channels list empty.
self.assertEmpty(xds_server.channels)
# Test fields set as is.
self.assertEqual(xds_server.ip, CANNED_IP)
self.assertEqual(xds_server.rpc_port, CANNED_RPC_PORT)
self.assertEqual(xds_server.hostname, CANNED_HOSTNAME)
# Test optional argument defaults.
self.assertEqual(xds_server.rpc_host, CANNED_IP)
self.assertEqual(xds_server.maintenance_port, CANNED_RPC_PORT)
self.assertEqual(xds_server.secure_mode, False)
def test_xds_address(self):
"""Verifies the behavior of set_xds_address(), xds_address, xds_uri."""
xds_server = XdsTestServer(
ip=CANNED_IP,
rpc_port=CANNED_RPC_PORT,
hostname=CANNED_HOSTNAME,
)
self.assertEqual(xds_server.xds_uri, "", msg="Must be empty when unset")
xds_server.set_xds_address(CANNED_XDS_HOST, CANNED_XDS_PORT)
self.assertEqual(xds_server.xds_uri, "xds:///xds-test-server:42")
xds_server.set_xds_address(CANNED_XDS_HOST, None)
self.assertEqual(
xds_server.xds_uri,
"xds:///xds-test-server",
msg="Must not contain ':port' when the port is not set",
)
xds_server.set_xds_address(None, None)
self.assertEqual(xds_server.xds_uri, "", msg="Must be empty when reset")
xds_server.set_xds_address(None, CANNED_XDS_PORT)
self.assertEqual(
xds_server.xds_uri,
"",
msg="Must be empty when only port is set",
)
if __name__ == "__main__":
absltest.main()
Loading…
Cancel
Save