Skip to content

Commit 1708f63

Browse files
authored
[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
1 parent 440eef2 commit 1708f63

File tree

6 files changed

+261
-2
lines changed

6 files changed

+261
-2
lines changed

.github/workflows/psm-interop.yaml

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
name: PSM Interop
2+
3+
on:
4+
pull_request:
5+
push:
6+
branches:
7+
- master
8+
- 'v1.*'
9+
10+
permissions:
11+
contents: read
12+
13+
jobs:
14+
unittest:
15+
# By default, only version is printed out in parens, e.g. "unittest (3.10)"
16+
# This changes it to "unittest (python3.10)"
17+
name: "unittest (python${{ matrix.python_version }})"
18+
runs-on: ubuntu-latest
19+
strategy:
20+
matrix:
21+
python_version: ["3.9", "3.10"]
22+
fail-fast: false
23+
permissions:
24+
pull-requests: read # Used by paths-filter to read the diff.
25+
defaults:
26+
run:
27+
working-directory: 'tools/run_tests/xds_k8s_test_driver'
28+
29+
steps:
30+
- uses: actions/checkout@v3
31+
32+
# To add this job to required GitHub checks, it's not enough to use
33+
# the on.pull_request.paths filter. For required checks, the job needs to
34+
# return the success status, and not be skipped.
35+
# Using paths-filter action, we skip the setup/test steps when psm interop
36+
# files are unchanged, and the job returns success.
37+
- uses: dorny/paths-filter@v2
38+
id: paths_filter
39+
with:
40+
filters: |
41+
psm_interop_src:
42+
- 'tools/run_tests/xds_k8s_test_driver/**'
43+
- 'src/proto/grpc/testing/empty.proto'
44+
- 'src/proto/grpc/testing/messages.proto'
45+
- 'src/proto/grpc/testing/test.proto'
46+
47+
- uses: actions/setup-python@v4
48+
if: ${{ steps.paths_filter.outputs.psm_interop_src == 'true' }}
49+
with:
50+
python-version: "${{ matrix.python_version }}"
51+
cache: 'pip'
52+
cache-dependency-path: 'tools/run_tests/xds_k8s_test_driver/requirements.lock'
53+
54+
- name: "Install requirements"
55+
if: ${{ steps.paths_filter.outputs.psm_interop_src == 'true' }}
56+
run: |
57+
pip list
58+
pip install --upgrade pip setuptools
59+
pip list
60+
pip install -r requirements.lock
61+
pip list
62+
63+
- name: "Generate protos"
64+
if: ${{ steps.paths_filter.outputs.psm_interop_src == 'true' }}
65+
run: >
66+
python -m grpc_tools.protoc --proto_path=../../../
67+
--python_out=. --grpc_python_out=.
68+
src/proto/grpc/testing/empty.proto
69+
src/proto/grpc/testing/messages.proto
70+
src/proto/grpc/testing/test.proto
71+
72+
- name: "Run unit tests"
73+
if: ${{ steps.paths_filter.outputs.psm_interop_src == 'true' }}
74+
run: python -m tests.unit

tools/run_tests/xds_k8s_test_driver/run.sh

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,5 +63,11 @@ export PYTHONPATH="${XDS_K8S_DRIVER_DIR}"
6363
# Split path to python file from the rest of the args.
6464
readonly PY_FILE="$1"
6565
shift
66-
# Append args after --flagfile, so they take higher priority.
67-
exec python "${PY_FILE}" --flagfile="${XDS_K8S_CONFIG}" "$@"
66+
67+
if [[ "${PY_FILE}" =~ tests/unit($|/) ]]; then
68+
# Do not set the flagfile when running unit tests.
69+
exec python "${PY_FILE}" "$@"
70+
else
71+
# Append args after --flagfile, so they take higher priority.
72+
exec python "${PY_FILE}" --flagfile="${XDS_K8S_CONFIG}" "$@"
73+
fi
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# Copyright 2023 The gRPC Authors
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
"""Discover and run all unit tests recursively."""
15+
16+
import pathlib
17+
18+
from absl.testing import absltest
19+
20+
21+
def load_tests(loader: absltest.TestLoader, unused_tests, unused_pattern):
22+
unit_tests_root = pathlib.Path(__file__).parent
23+
return loader.discover(f"{unit_tests_root}", pattern="*_test.py")
24+
25+
26+
if __name__ == "__main__":
27+
absltest.main()
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# Copyright 2023 gRPC authors.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# Copyright 2023 gRPC authors.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
from typing import Optional
15+
16+
from absl.testing import absltest
17+
18+
from framework.test_app import client_app
19+
20+
# Alias
21+
XdsTestClient = client_app.XdsTestClient
22+
23+
# Test values.
24+
CANNED_IP: str = "10.0.0.42"
25+
CANNED_RPC_PORT: int = 1111
26+
CANNED_HOSTNAME: str = "test-client.local"
27+
CANNED_SERVER_TARGET: str = "xds:///test-server"
28+
29+
30+
class ClientAppTest(absltest.TestCase):
31+
"""Unit test for the ClientApp."""
32+
33+
def test_constructor(self):
34+
xds_client = XdsTestClient(
35+
ip=CANNED_IP,
36+
rpc_port=CANNED_RPC_PORT,
37+
hostname=CANNED_HOSTNAME,
38+
server_target=CANNED_SERVER_TARGET,
39+
)
40+
# Channels list empty.
41+
self.assertEmpty(xds_client.channels)
42+
43+
# Test fields set as is.
44+
self.assertEqual(xds_client.ip, CANNED_IP)
45+
self.assertEqual(xds_client.rpc_port, CANNED_RPC_PORT)
46+
self.assertEqual(xds_client.server_target, CANNED_SERVER_TARGET)
47+
self.assertEqual(xds_client.hostname, CANNED_HOSTNAME)
48+
49+
# Test optional argument defaults.
50+
self.assertEqual(xds_client.rpc_host, CANNED_IP)
51+
self.assertEqual(xds_client.maintenance_port, CANNED_RPC_PORT)
52+
53+
54+
if __name__ == "__main__":
55+
absltest.main()
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
# Copyright 2023 gRPC authors.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
from typing import Optional
15+
16+
from absl.testing import absltest
17+
18+
from framework.test_app import server_app
19+
20+
# Alias
21+
XdsTestServer = server_app.XdsTestServer
22+
23+
# Test values.
24+
CANNED_IP: str = "10.0.0.43"
25+
CANNED_RPC_PORT: int = 2222
26+
CANNED_HOSTNAME: str = "test-server.local"
27+
CANNED_XDS_HOST: str = "xds-test-server"
28+
CANNED_XDS_PORT: int = 42
29+
30+
31+
class ServerAppTest(absltest.TestCase):
32+
"""Unit test for the XdsTestServer."""
33+
34+
def test_constructor(self):
35+
xds_server = XdsTestServer(
36+
ip=CANNED_IP,
37+
rpc_port=CANNED_RPC_PORT,
38+
hostname=CANNED_HOSTNAME,
39+
)
40+
# Channels list empty.
41+
self.assertEmpty(xds_server.channels)
42+
43+
# Test fields set as is.
44+
self.assertEqual(xds_server.ip, CANNED_IP)
45+
self.assertEqual(xds_server.rpc_port, CANNED_RPC_PORT)
46+
self.assertEqual(xds_server.hostname, CANNED_HOSTNAME)
47+
48+
# Test optional argument defaults.
49+
self.assertEqual(xds_server.rpc_host, CANNED_IP)
50+
self.assertEqual(xds_server.maintenance_port, CANNED_RPC_PORT)
51+
self.assertEqual(xds_server.secure_mode, False)
52+
53+
def test_xds_address(self):
54+
"""Verifies the behavior of set_xds_address(), xds_address, xds_uri."""
55+
xds_server = XdsTestServer(
56+
ip=CANNED_IP,
57+
rpc_port=CANNED_RPC_PORT,
58+
hostname=CANNED_HOSTNAME,
59+
)
60+
self.assertEqual(xds_server.xds_uri, "", msg="Must be empty when unset")
61+
62+
xds_server.set_xds_address(CANNED_XDS_HOST, CANNED_XDS_PORT)
63+
self.assertEqual(xds_server.xds_uri, "xds:///xds-test-server:42")
64+
65+
xds_server.set_xds_address(CANNED_XDS_HOST, None)
66+
self.assertEqual(
67+
xds_server.xds_uri,
68+
"xds:///xds-test-server",
69+
msg="Must not contain ':port' when the port is not set",
70+
)
71+
72+
xds_server.set_xds_address(None, None)
73+
self.assertEqual(xds_server.xds_uri, "", msg="Must be empty when reset")
74+
75+
xds_server.set_xds_address(None, CANNED_XDS_PORT)
76+
self.assertEqual(
77+
xds_server.xds_uri,
78+
"",
79+
msg="Must be empty when only port is set",
80+
)
81+
82+
83+
if __name__ == "__main__":
84+
absltest.main()

0 commit comments

Comments
 (0)