Skip to content

Commit 8bccc20

Browse files
authored
Added unit tests for the example applications.
Serial port, Septentrio, and NTRIP server examples not currently implemented. Merge pull request #37.
2 parents bf75b11 + 69bf71d commit 8bccc20

13 files changed

+324
-14
lines changed

.github/workflows/release_build.yml

+22-5
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,14 @@ jobs:
135135
bazel build -c opt --config=${{ matrix.arch }} //:polaris_client
136136
bazel build -c opt --config=${{ matrix.arch }} //examples:*
137137
138+
# Run unit tests.
139+
- name: Run Unit Tests
140+
if: matrix.arch == 'x64'
141+
env:
142+
POLARIS_API_KEY: ${{ secrets.POLARIS_API_KEY }}
143+
run: |
144+
./test/run_unit_tests.sh --tool=${{ matrix.tool }} --unique-id-prefix=${{ matrix.tool }}_cpp
145+
138146
# Package artifacts (Bazel only -- no need to do this for multiple build
139147
# tools).
140148
- name: Create artifact
@@ -241,16 +249,25 @@ jobs:
241249
bazel build -c opt --config=${{ matrix.arch }} //:polaris_client
242250
bazel build -c opt --config=${{ matrix.arch }} //examples:*
243251
244-
# Package artifacts (GNU Make only -- no need to do this for multiple build
252+
# Run unit tests.
253+
- name: Run Unit Tests
254+
if: matrix.arch == 'x64'
255+
env:
256+
POLARIS_API_KEY: ${{ secrets.POLARIS_API_KEY }}
257+
run: |
258+
./test/run_unit_tests.sh --tool=${{ matrix.tool }} --unique-id-prefix=${{ matrix.tool }}_c_
259+
260+
# Package artifacts (Bazel only -- no need to do this for multiple build
245261
# tools).
246262
- name: Create artifact
247-
if: matrix.tool == 'make'
263+
if: matrix.tool == 'bazel'
248264
run: |
249-
make print_applications |
250-
xargs tar czfv polaris_examples.tar.gz --transform 's|^|polaris/c/|'
265+
bazel query 'kind("cc_binary", //examples:*)' 2>/dev/null |
266+
sed -e 's|//examples:|bazel-bin/examples/|' |
267+
xargs tar czf polaris_examples.tar.gz --transform 's|^bazel-bin|polaris/c|'
251268
252269
- name: Upload artifact
253-
if: matrix.tool == 'make'
270+
if: matrix.tool == 'bazel'
254271
uses: actions/upload-artifact@v1
255272
with:
256273
path: c/polaris_examples.tar.gz

.gitignore

+3
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ bazel-*
44
# CMake
55
build/
66

7+
# Python
8+
*.pyc
9+
710
# Visual Studio Code
811
.vscode
912
# Clion

c/src/point_one/polaris/polaris.c

+3-3
Original file line numberDiff line numberDiff line change
@@ -177,15 +177,15 @@ int Polaris_Authenticate(PolarisContext_t* context, const char* api_key,
177177
}
178178

179179
if (strlen(unique_id) > POLARIS_MAX_UNIQUE_ID_SIZE) {
180-
P1_Print("Unique ID must be a maximum of %d characters.\n",
181-
POLARIS_MAX_UNIQUE_ID_SIZE);
180+
P1_Print("Unique ID must be a maximum of %d characters. [id='%s']\n",
181+
POLARIS_MAX_UNIQUE_ID_SIZE, unique_id);
182182
return POLARIS_ERROR;
183183
} else {
184184
for (const char* ptr = unique_id; *ptr != '\0'; ++ptr) {
185185
char c = *ptr;
186186
if (c != '-' && c != '_' && (c < 'A' || c > 'Z') &&
187187
(c < 'a' || c > 'z') && (c < '0' || c > '9')) {
188-
P1_Print("Invalid unique ID specified.\n");
188+
P1_Print("Invalid unique ID specified. [id='%s']\n", unique_id);
189189
return POLARIS_ERROR;
190190
}
191191
}

c/test/application_base.py

+227
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
1+
from argparse import ArgumentParser
2+
import copy
3+
import os
4+
import re
5+
import signal
6+
import subprocess
7+
import sys
8+
import threading
9+
10+
11+
class TestApplicationBase(object):
12+
TEST_PASSED = 0
13+
TEST_FAILED = 1
14+
ARGUMENT_ERROR = 2
15+
EXECUTION_ERROR = 3
16+
NONZERO_EXIT = 4
17+
18+
DEFAULT_ROOT_DIR = os.path.abspath(os.path.normpath(os.path.join(os.path.dirname(__file__), '..')))
19+
DEFAULT_COMMAND = ['%(path)s', '%(polaris_api_key)s', '%(unique_id)s']
20+
21+
def __init__(self, application_name, root_dir=None):
22+
if root_dir is None:
23+
self.root_dir = self.DEFAULT_ROOT_DIR
24+
else:
25+
self.root_dir = root_dir
26+
27+
self.application_name = application_name
28+
29+
# Define and parse arguments.
30+
self.parser = ArgumentParser(usage='%(prog)s [OPTIONS]...')
31+
32+
self.parser.add_argument(
33+
'-p', '--path', metavar='PATH',
34+
help="The path to the application to be run.")
35+
self.parser.add_argument(
36+
'--polaris-api-key', metavar='KEY',
37+
help="The Polaris API key to be used. If not set, defaults to the POLARIS_API_KEY environment variable if "
38+
"specified.")
39+
self.parser.add_argument(
40+
'-t', '--timeout', metavar='SEC', type=float, default=30.0,
41+
help="The maximum test duration (in seconds).")
42+
self.parser.add_argument(
43+
'--tool', metavar='TOOL', default='bazel',
44+
help="The tool used to compile the application (bazel, cmake, make), used to determine the default "
45+
"application path. Ignored if --path is specified.")
46+
self.parser.add_argument(
47+
'--unique-id', metavar='ID', default=application_name,
48+
help="The unique ID to assign to this instance. The ID will be prepended with PREFIX if --unique-id-prefix "
49+
"is specified.")
50+
self.parser.add_argument(
51+
'--unique-id-prefix', metavar='PREFIX',
52+
help="An optional prefix to prepend to the unique ID.")
53+
54+
self.options = None
55+
56+
self.program_args = []
57+
58+
self.proc = None
59+
60+
def parse_args(self):
61+
self.options = self.parser.parse_args()
62+
63+
if self.options.polaris_api_key is None:
64+
self.options.polaris_api_key = os.getenv('POLARIS_API_KEY')
65+
if self.options.polaris_api_key is None:
66+
print('Error: Polaris API key not specified.')
67+
sys.exit(self.ARGUMENT_ERROR)
68+
69+
if self.options.path is None:
70+
if self.options.tool == 'bazel':
71+
self.options.path = os.path.join(self.root_dir, 'bazel-bin/examples', self.application_name)
72+
elif self.options.tool == 'cmake':
73+
for build_dir in ('build', 'cmake_build'):
74+
path = os.path.join(self.root_dir, build_dir, 'examples', self.application_name)
75+
if os.path.exists(path):
76+
self.options.path = path
77+
break
78+
if self.options.path is None:
79+
print('Error: Unable to locate CMake build directory.')
80+
sys.exit(self.ARGUMENT_ERROR)
81+
elif self.options.tool == 'make':
82+
self.options.path = os.path.join(self.root_dir, 'examples', self.application_name)
83+
else:
84+
print('Error: Unsupported --tool value.')
85+
sys.exit(self.ARGUMENT_ERROR)
86+
87+
if self.options.unique_id_prefix is not None:
88+
self.options.unique_id = self.options.unique_id_prefix + self.options.unique_id
89+
if len(self.options.unique_id) > 36:
90+
self.options.unique_id = self.options.unique_id[:36]
91+
print("Unique ID too long. Truncating to '%s'." % self.options.unique_id)
92+
93+
return self.options
94+
95+
def run(self, return_result=False):
96+
# Setup the command to be run.
97+
command = copy.deepcopy(self.DEFAULT_COMMAND)
98+
command.extend(self.program_args)
99+
api_key_standin = '%s...' % self.options.polaris_api_key[:4]
100+
for i in range(len(command)):
101+
if command[i].endswith('%(polaris_api_key)s'):
102+
# We temporarily replace the API key placeholder with the first 4 chars of the key before printing to
103+
# the console to avoid printing the actual key to the console. It will be swapped with the real key
104+
# below.
105+
command[i] = command[i].replace('%(polaris_api_key)s', api_key_standin)
106+
else:
107+
command[i] = command[i] % self.options.__dict__
108+
109+
print('Executing: %s' % ' '.join(command))
110+
111+
command.insert(0, 'stdbuf')
112+
command.insert(1, '-o0')
113+
for i in range(len(command)):
114+
if command[i].endswith(api_key_standin):
115+
command[i] = command[i].replace(api_key_standin, self.options.polaris_api_key)
116+
117+
# Run the command.
118+
def ignore_signal(sig, frame):
119+
signal.signal(sig, signal.SIG_DFL)
120+
121+
def preexec_function():
122+
# Disable forwarding of SIGINT/SIGTERM from the parent process (this script) to the child process (the
123+
# application under test).
124+
os.setpgrp()
125+
signal.signal(signal.SIGINT, ignore_signal)
126+
signal.signal(signal.SIGTERM, ignore_signal)
127+
128+
self.proc = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, encoding='utf-8',
129+
preexec_fn=preexec_function)
130+
131+
# Capture SIGINT and SIGTERM and shutdown the application gracefully.
132+
def request_shutdown(sig, frame):
133+
self.stop()
134+
signal.signal(sig, signal.SIG_DFL)
135+
136+
signal.signal(signal.SIGINT, request_shutdown)
137+
signal.signal(signal.SIGTERM, request_shutdown)
138+
139+
# Stop the test after a max duration.
140+
def timeout_elapsed():
141+
print('Maximum test duration (%.1f sec) elapsed.' % self.options.timeout)
142+
self.stop()
143+
144+
watchdog = threading.Timer(self.options.timeout, timeout_elapsed)
145+
watchdog.start()
146+
147+
# Check for a pass/fail condition and forward output to the console.
148+
while True:
149+
try:
150+
line = self.proc.stdout.readline().rstrip('\n')
151+
if line != '':
152+
print(line.rstrip('\n'))
153+
self.on_stdout(line)
154+
elif self.proc.poll() is not None:
155+
exit_code = self.proc.poll()
156+
break
157+
except KeyboardInterrupt:
158+
print('Execution interrupted unexpectedly.')
159+
if return_result:
160+
return self.EXECUTION_ERROR
161+
else:
162+
sys.exit(self.EXECUTION_ERROR)
163+
164+
watchdog.cancel()
165+
self.proc = None
166+
167+
result = self.check_pass_fail(exit_code)
168+
if result == self.TEST_PASSED:
169+
print('Test result: success')
170+
else:
171+
print('Test result: FAIL')
172+
173+
if return_result:
174+
return result
175+
else:
176+
sys.exit(result)
177+
178+
def stop(self):
179+
if self.proc is not None:
180+
print('Sending shutdown request to the application.')
181+
self.proc.terminate()
182+
183+
def check_pass_fail(self, exit_code):
184+
if exit_code != 0:
185+
print('Application exited with non-zero exit code %s.' % repr(exit_code))
186+
return self.NONZERO_EXIT
187+
else:
188+
return self.TEST_PASSED
189+
190+
def on_stdout(self, line):
191+
pass
192+
193+
194+
class StandardApplication(TestApplicationBase):
195+
"""!
196+
@brief Unit test for an example application that prints a data received
197+
message and requires no outside input.
198+
199+
The data received message must be formatted as:
200+
```
201+
Application received N bytes.
202+
```
203+
"""
204+
205+
def __init__(self, application_name):
206+
super().__init__(application_name=application_name)
207+
self.data_received = False
208+
209+
def check_pass_fail(self, exit_code):
210+
# Note: There is currently a race condition when the subprocess is shutdown (SIGTERM) where either the
211+
# application itself exits cleanly with code 0 as expected, or the Python fork running it exits first with
212+
# -SIGTERM before the application gets a chance to exit. The preexec stuff above doesn't seem to be enough to
213+
# fix it. For now, we simply treat the combination of -SIGTERM + data received as a pass.
214+
if exit_code == 0 or exit_code == -signal.SIGTERM:
215+
if self.data_received:
216+
return self.TEST_PASSED
217+
else:
218+
print('No corrections data received.')
219+
return self.TEST_FAILED
220+
else:
221+
return super().check_pass_fail(exit_code)
222+
223+
def on_stdout(self, line):
224+
if re.match(r'.*Application received \d+ bytes.', line):
225+
print('Corrections data detected.')
226+
self.data_received = True
227+
self.stop()

c/test/run_unit_tests.sh

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
#!/bin/bash
2+
3+
set -e
4+
5+
echo "Testing simple_polaris_client..."
6+
python3 test/test_simple_polaris_client.py $*
7+
8+
echo "Testing connection_retry..."
9+
python3 test/test_connection_retry.py $*

c/test/test_connection_retry.py

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
#!/usr/bin/env python3
2+
3+
from application_base import StandardApplication
4+
5+
class Test(StandardApplication):
6+
def __init__(self):
7+
super().__init__(application_name='connection_retry')
8+
9+
test = Test()
10+
test.parse_args()
11+
test.run()

c/test/test_simple_polaris_client.py

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
#!/usr/bin/env python3
2+
3+
from application_base import StandardApplication
4+
5+
class Test(StandardApplication):
6+
def __init__(self):
7+
super().__init__(application_name='simple_polaris_client')
8+
9+
test = Test()
10+
test.parse_args()
11+
test.run()

examples/BUILD

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ package(default_visibility = ["//visibility:public"])
22

33
# Simple example of connecting to the Polaris service.
44
cc_binary(
5-
name = "simple_polaris_client",
5+
name = "simple_polaris_cpp_client",
66
srcs = ["simple_polaris_client.cc"],
77
deps = [
88
"//:polaris_client",

examples/simple_polaris_client.cc

+1-1
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ PolarisClient* polaris_client = nullptr;
3636
// Process receiver incoming messages. This example code expects received data
3737
// to be ascii nmea messages.
3838
void ReceivedData(const uint8_t* data, size_t length) {
39-
LOG(INFO) << "Received " << length << " bytes.";
39+
LOG(INFO) << "Application received " << length << " bytes.";
4040
}
4141

4242
void HandleSignal(int sig) {

readme.md

+4-4
Original file line numberDiff line numberDiff line change
@@ -361,7 +361,7 @@ they are more commonly run using the `bazel run` command. For example, to run th
361361
[Simple Polaris Client](#simple-polaris-client-1) example application, run the following:
362362

363363
```bash
364-
bazel run -c opt //examples:simple_polaris_client -- --polaris_api_key=<POLARIS_API_KEY>
364+
bazel run -c opt //examples:simple_polaris_cpp_client -- --polaris_api_key=<POLARIS_API_KEY>
365365
```
366366

367367
See [Simple Polaris Client](#simple-polaris-client-1) for more details.
@@ -375,7 +375,7 @@ specify the `--config` argument to Bazel with one of the following values:
375375

376376
For example:
377377
```
378-
bazel build --config=aarch64 //examples:simple_polaris_client
378+
bazel build --config=aarch64 //examples:simple_polaris_cpp_client
379379
```
380380

381381
#### CMake ####
@@ -407,7 +407,7 @@ example, to run the [Simple Polaris Client](#simple-polaris-client-1) example ap
407407
run the following:
408408

409409
```bash
410-
./examples/simple_polaris_client --polaris_api_key=<POLARIS_API_KEY>
410+
./examples/simple_polaris_cpp_client --polaris_api_key=<POLARIS_API_KEY>
411411
```
412412

413413
See [Simple Polaris Client](#simple-polaris-client-1) for more details.
@@ -477,7 +477,7 @@ A small example of establishing a Polaris connection and receiving RTCM correcti
477477

478478
To run the application, run the following command:
479479
```
480-
bazel run //examples:simple_polaris_client -- --polaris_api_key=<POLARIS_API_KEY>
480+
bazel run //examples:simple_polaris_cpp_client -- --polaris_api_key=<POLARIS_API_KEY>
481481
```
482482
where `<POLARIS_API_KEY>` is the API key assigned to you by Point One. The application uses a built-in unique ID by
483483
default, but you may change the unique ID using the `--polaris_unique_id` argument. See

0 commit comments

Comments
 (0)