Changeset 11127 in ntrip
- Timestamp:
- Oct 24, 2026, 11:15:22 PM (5 hours ago)
- Location:
- trunk/BNC/tests
- Files:
-
- 2 added
- 6 edited
-
README.md (modified) (3 diffs)
-
cases/upload_rtnet/case.json (modified) (2 diffs)
-
cases/upload_rtnet/expected/upload_crs.txt.gz (added)
-
run_tests.py (modified) (4 diffs)
-
tools/bncraw.py (modified) (4 diffs)
-
tools/fakecaster.py (modified) (7 diffs)
-
tools/ssl_check.py (added)
-
tools/ssr_harness.cpp (modified) (4 diffs)
Legend:
- Unmodified
- Added
- Removed
-
trunk/BNC/tests/README.md
r11124 r11127 26 26 | `cases/<name>/expected/*.gz` | normalised reference outputs | 27 27 | `data/*.raw` | short raw captures (BNC `rawOutFile` format) replayed via `--file` | 28 | `tools/fakecaster.py` | local Ntrip caster recording an upload as a BNC raw file (case key `fakecaster`) | 29 | `tools/bncraw.py` | `info` (streams, time span, RTCM3 message types) and`slice` for raw captures |28 | `tools/fakecaster.py` | local Ntrip caster recording an upload as a BNC raw file (case key `fakecaster`); optionally TLS and serving a stream | 29 | `tools/bncraw.py` | `info` (streams, time span, RTCM3 message types), `slice` and `crs` (decode 1300/1301/1302) for raw captures | 30 30 | `tools/mkcrafted.py` | writes `data/crafted_limits.raw`: crafted frames at the upper field limits | 31 31 | `tools/ssr_harness.sh` | builds `tools/ssr_harness.cpp` with the SSR decoders under ASan/UBSan and runs it | 32 | `tools/ssl_check.py` | TLS checks of the Ntrip 2s client and upload (not part of `run_tests.py`) | 32 33 | `work/` | scratch output, not versioned | 33 34 … … 75 76 (`tools/fakecaster.py`, Ntrip 1/2, chunked), and a `post` step decodes the 76 77 received stream again: `upload_rtnet` (IGS-SSR encoding, SP3/CLK/BIA files, 77 decoded `.ssr`), `upload_eph` (RTCM3 ephemerides to RINEX nav) and 78 decoded `.ssr`, CRS messages 1300/1302 decoded by `bncraw.py crs` in a 79 `tool` post step), `upload_eph` (RTCM3 ephemerides to RINEX nav) and 78 80 `upload_raw` (raw forwarding to RINEX nav). `upload_rtnet_rtcm` encodes the same input to RTCM-SSR 79 81 (message numbers 1057-1068, 1240-1270) and decodes it again. … … 114 116 `crafted_limits.raw`: 7 entries with additional parameters of 40 and 0 bits, 115 117 and 31 entries, which must be rejected), which `RTCM3Decoder.cpp` does not 116 dispatch at present. Exit code 0 only without sanitizer errors. 118 dispatch at present. Every decoded VTEC and metadata message is encoded 119 again with the `Make*` function of the same class and decoded once more; the 120 result must be identical. Exit code 0 only without sanitizer errors and 121 differences. 122 123 ## TLS check 124 125 ``` 126 tests/tools/ssl_check.py [--bnc PATH] 127 ``` 128 129 Runs BNC against `tools/fakecaster.py` with TLS on port 443 (BNC uploads with 130 Ntrip 2s always to 443) inside an unprivileged user and network namespace 131 (`unshare -rn`), with a test CA, server and client certificates created by 132 openssl in `work/ssl_check/`. Checks for upload and download: certificate of 133 an unknown CA rejected, accepted with the CA in `sslCaCertPath`, accepted with 134 `sslIgnoreErrors` and the ignored errors logged; for upload also a 135 certificate for another host name (rejected) and a client certificate 136 required by the caster, with and without `sslIgnoreErrors`. Takes about 3 min. 117 137 118 138 ## Adding a replay case -
trunk/BNC/tests/cases/upload_rtnet/case.json
r11074 r11127 1 1 { 2 "description": "16_Upload replay: RTNET orbits/clocks/biases + BCEP00BKG0 encoded to IGS-SSR and uploaded (Ntrip 2, chunked) to a local fake caster; SP3/CLK/BIA written; the uploaded stream is decoded again (03_BrdcCorr), 5 min", 2 "description": "16_Upload replay: RTNET orbits/clocks/biases + BCEP00BKG0 encoded to IGS-SSR and uploaded (Ntrip 2, chunked) to a local fake caster; SP3/CLK/BIA written; the uploaded stream is decoded again (03_BrdcCorr), CRS messages 1300/1302 decoded by bncraw.py crs, 5 min", 3 3 "config": "Example_Configs/16_Upload.bnc", 4 4 "raw": "data/upload_rtnet_5min.raw", … … 10 10 "time": "2026-10-01T09:33:00", "timeout": 10}, 11 11 "post": [{"dir": "dec", "config": "Example_Configs/03_BrdcCorr.bnc", 12 "raw": "Output/upload.raw", "exit_code": 5, "timeout": 300}], 12 "raw": "Output/upload.raw", "exit_code": 5, "timeout": 300}, 13 {"dir": "crs", "tool": ["bncraw.py", "crs", "Output/upload.raw"]}], 13 14 "outputs": ["Output/*.SP3", "Output/*.CLK", "Output/*.BIA", 14 15 {"glob": "Output/Upload.log_261001", "name": "Upload.log"}, 15 16 "dec/Output/*.ssr", 17 {"glob": "crs/stdout.txt", "name": "upload_crs.txt"}, 16 18 {"glob": "dec/stderr.txt", "name": "decode_trace.txt", "debug_trace": true}] 17 19 } -
trunk/BNC/tests/run_tests.py
r11069 r11127 30 30 {"dir" (subdirectory of the work dir), "config", "raw" 31 31 (relative to the work dir), "args", "exit_code", "timeout"}; 32 outputs may refer to their files as "<dir>/Output/..." 32 outputs may refer to their files as "<dir>/Output/..."; 33 instead of a BNC run, {"dir", "tool": [script in tools/, args]} 34 runs a Python tool in the work directory, stdout to <dir>/stdout.txt 33 35 34 36 BNC runs in tests/work/<name>/, which contains an "Input" symlink to … … 178 180 179 181 182 def run_tool(work, spec): 183 """Run tools/<script> with args in the work directory, stdout to 184 <dir>/stdout.txt. Returns the exit code or None on timeout.""" 185 outdir = os.path.join(work, spec["dir"]) 186 os.makedirs(outdir, exist_ok=True) 187 cmd = [sys.executable, "-I", os.path.join(TESTS, "tools", spec["tool"][0])] + spec["tool"][1:] 188 with open(os.path.join(outdir, "stdout.txt"), "wb") as out: 189 try: 190 return subprocess.run(cmd, cwd=work, stdout=out, stderr=subprocess.STDOUT, 191 timeout=spec.get("timeout", 600)).returncode 192 except subprocess.TimeoutExpired: 193 return None 194 195 180 196 def run_case(name, bnc, update): 181 197 res = Result(name) … … 217 233 # in its own subdirectory of the work directory, raw relative to work/ 218 234 for post in case.get("post", []): 219 pcode = run_bnc(bnc, os.path.join(work, post["dir"]), post, work) 235 if "tool" in post: 236 pcode = run_tool(work, post) 237 else: 238 pcode = run_bnc(bnc, os.path.join(work, post["dir"]), post, work) 220 239 if pcode is None or pcode != post.get("exit_code", 0): 221 240 res.fail("post step %s: exit code %s" % (post["dir"], pcode)) … … 250 269 expfile = os.path.join(expdir, ref + ".gz") 251 270 if update: 271 if spec.get("debug_trace") and not actual.strip() and os.path.exists(expfile): 272 # build without BNC_DEBUG_SSR: keep the existing trace reference 273 written.add(os.path.basename(expfile)) 274 res.skip("%s: no BNC_DEBUG_SSR trace in this build, reference kept" % ref) 275 continue 252 276 write_expected(expfile, actual) 253 277 written.add(os.path.basename(expfile)) -
trunk/BNC/tests/tools/bncraw.py
r11069 r11127 9 9 info <raw> [<raw> ...] streams, byte counts, time span, RTCM3 message types 10 10 slice <raw> <out> [options] cut a time window / subset of stations 11 crs <raw> decode RTCM 1300/1301/1302 (CRS messages) 11 12 """ 12 13 … … 110 111 return "4076_%03d" % sub 111 112 return str(t) 113 114 115 class BitReader: 116 def __init__(self, data): 117 self.data = data 118 self.pos = 0 119 120 def u(self, n): 121 v = 0 122 for _ in range(n): 123 if self.pos >= 8 * len(self.data): 124 raise ValueError("message too short") 125 v = (v << 1) | ((self.data[self.pos // 8] >> (7 - self.pos % 8)) & 1) 126 self.pos += 1 127 return v 128 129 def s(self, n): 130 v = self.u(n) 131 return v - (1 << n) if v >> (n - 1) else v 132 133 def text(self): 134 n = self.u(5) 135 return bytes(self.u(8) for _ in range(n)).decode("latin-1") 136 137 def left(self): 138 return 8 * len(self.data) - self.pos 139 140 141 def decode_crs(p): 142 """One line per 1300/1301/1302 message (RTCM 10403.4, DF562-DF587).""" 143 r = BitReader(p) 144 t = r.u(12) 145 if t == 1300: 146 out = "1300 name %r CE %.2f" % (r.text(), r.u(16) / 100.0) 147 elif t == 1301: 148 src, tgt = r.text(), r.text() 149 out = "1301 source %r target %r sys %d util %d mjd %d par" % ( 150 src, tgt, r.u(8), r.u(10), r.u(16) + 44244) 151 for n in (23, 23, 23, 32, 32, 32, 25, 17, 17, 17, 17, 17, 17, 14): 152 out += " %d" % r.s(n) 153 else: 154 out = "1302 name %r anchor %d plate %d links" % (r.text(), r.u(1), r.u(5)) 155 out += " %r" % [r.text() for _ in range(r.u(3))] 156 if r.left() >= 8: # more than the padding to the byte boundary 157 out += " TRAILING %d bits" % r.left() 158 return out 112 159 113 160 … … 166 213 167 214 215 def cmd_crs(args): 216 """Distinct CRS message contents per stream, with their number.""" 217 stats = collections.OrderedDict() 218 framers = {} 219 for t, sta, fmt, data in read_chunks(args.input): 220 for p in framers.setdefault(sta, Rtcm3Framer()).feed(data): 221 if msg_type(p) in ("1300", "1301", "1302"): 222 try: 223 line = decode_crs(p) 224 except ValueError as e: 225 line = "%s %s" % (msg_type(p), e) 226 key = (sta, line) 227 stats[key] = stats.get(key, 0) + 1 228 for (sta, line), n in stats.items(): 229 print("%s %4dx %s" % (sta, n, line)) 230 231 168 232 def main(): 169 233 ap = argparse.ArgumentParser(description=__doc__, … … 183 247 p.set_defaults(func=cmd_slice) 184 248 249 p = sub.add_parser("crs", help="decode CRS messages 1300/1301/1302") 250 p.add_argument("input") 251 p.set_defaults(func=cmd_crs) 252 185 253 args = ap.parse_args() 186 254 args.func(args) -
trunk/BNC/tests/tools/fakecaster.py
r11074 r11127 11 11 12 12 fakecaster.py --port 21011 --out upload.raw --sta CLOCK --time 2026-10-01T09:30:00 13 14 With --tls-cert/--tls-key the caster speaks TLS (Ntrip 2s); --tls-client-ca 15 additionally requires a client certificate signed by that CA. With --serve 16 a GET request (BNC as Ntrip client) is answered with the RTCM bytes of the 17 given raw file. Failed TLS handshakes are reported on stdout. 13 18 """ 14 19 15 20 import argparse 21 import os 16 22 import socket 23 import ssl 17 24 import sys 18 25 import threading 26 27 sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) 19 28 20 29 … … 45 54 self.head = "%s %s %s" % (time, sta, fmt) 46 55 self.total = 0 56 self.handshakes = 0 57 self.served = 0 47 58 48 59 def write(self, data): … … 56 67 57 68 58 def serve(conn, store, timeout): 69 def serve(conn, store, timeout, tls=None, served=None): 59 70 conn.settimeout(timeout) 60 71 buf = b"" 61 72 try: 73 if tls: 74 try: 75 conn = tls.wrap_socket(conn, server_side=True) 76 except (ssl.SSLError, OSError) as e: 77 print("tls handshake failed: %s" % e, flush=True) 78 return 79 store.handshakes += 1 62 80 while b"\r\n\r\n" not in buf: 63 81 d = conn.recv(4096) … … 67 85 header, _, pending = buf.partition(b"\r\n\r\n") 68 86 chunked = b"chunked" in header.lower() 87 if header.startswith(b"GET") and served is not None: 88 conn.sendall(b"HTTP/1.1 200 OK\r\nNtrip-Version: Ntrip/2.0\r\n" 89 b"Content-Type: gnss/data\r\nConnection: close\r\n\r\n" + served) 90 store.served += len(served) 91 return 69 92 if header.startswith(b"SOURCE"): 70 93 conn.sendall(b"ICY 200 OK\r\n\r\n") … … 100 123 ap.add_argument("--timeout", type=float, default=600.0, 101 124 help="stop after this many seconds without a new connection or data") 125 ap.add_argument("--tls-cert", help="server certificate (PEM): TLS") 126 ap.add_argument("--tls-key", help="server private key (PEM)") 127 ap.add_argument("--tls-client-ca", help="require a client certificate of this CA") 128 ap.add_argument("--serve", help="raw file whose RTCM bytes are sent on GET") 102 129 args = ap.parse_args() 130 131 tls = None 132 if args.tls_cert: 133 tls = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) 134 tls.load_cert_chain(args.tls_cert, args.tls_key) 135 if args.tls_client_ca: 136 tls.verify_mode = ssl.CERT_REQUIRED 137 tls.load_verify_locations(args.tls_client_ca) 138 served = None 139 if args.serve: 140 from bncraw import read_chunks # noqa: E402 141 served = b"".join(d for t, sta, fmt, d in read_chunks(args.serve)) 103 142 104 143 store = Store(args.out, args.sta, args.format, args.time) … … 116 155 except socket.timeout: 117 156 break 118 t = threading.Thread(target=serve, args=(conn, store, args.timeout), daemon=True) 157 t = threading.Thread(target=serve, args=(conn, store, args.timeout, tls, served), 158 daemon=True) 119 159 t.start() 120 160 threads.append(t) … … 122 162 t.join(args.timeout) 123 163 print("received %d bytes in %d connection(s)" % (store.total, len(threads)), flush=True) 164 if tls: 165 print("%d TLS handshake(s) completed" % store.handshakes, flush=True) 166 if served is not None: 167 print("served %d bytes" % store.served, flush=True) 124 168 return 0 125 169 -
trunk/BNC/tests/tools/ssr_harness.cpp
r11125 r11127 6 6 // struct is a separate heap block here, so a write beyond a struct is 7 7 // reported by AddressSanitizer instead of silently corrupting the next member. 8 // 9 // Every decoded VTEC (all formats) and metadata message (new RTCM-SSR) is 10 // encoded again with the Make* function of the same class and decoded once 11 // more; the result must be identical (exit code 1 otherwise). 8 12 // 9 13 // Usage: ssr_harness <file> (BNC raw file or plain RTCM3 byte stream) … … 33 37 } 34 38 39 struct t_target { 40 t_target() : co(new SsrCorr::ClockOrbit()), cb(new SsrCorr::CodeBias()), 41 v(new SsrCorr::VTEC()), pb(new SsrCorr::PhaseBias()), 42 md(new SsrCorr::MetaData()), a(new SsrCorr::Antenna()) {} 43 ~t_target() {delete co; delete cb; delete v; delete pb; delete md; delete a;} 44 int decode(SsrCorr* ssr, const unsigned char* frame, size_t size) { 45 int bytesused = 0; 46 return ssr->GetSSR(co, cb, v, pb, md, a, (const char*)frame, size, &bytesused); 47 } 48 SsrCorr::ClockOrbit* co; 49 SsrCorr::CodeBias* cb; 50 SsrCorr::VTEC* v; 51 SsrCorr::PhaseBias* pb; 52 SsrCorr::MetaData* md; 53 SsrCorr::Antenna* a; 54 }; 55 56 static bool sameVTEC(const SsrCorr::VTEC* v1, const SsrCorr::VTEC* v2) { 57 if (v1->EpochTime != v2->EpochTime || v1->NumLayers != v2->NumLayers || 58 v1->Quality != v2->Quality) { 59 return false; 60 } 61 for (unsigned l = 0; l < v1->NumLayers; l++) { 62 const SsrCorr::VTEC::IonoLayers& l1 = v1->Layers[l]; 63 const SsrCorr::VTEC::IonoLayers& l2 = v2->Layers[l]; 64 if (l1.Height != l2.Height || l1.Degree != l2.Degree || l1.Order != l2.Order) { 65 return false; 66 } 67 for (unsigned d = 0; d <= l1.Degree; d++) { 68 for (unsigned o = 0; o <= l1.Order; o++) { 69 if (l1.Cosinus[d][o] != l2.Cosinus[d][o] || l1.Sinus[d][o] != l2.Sinus[d][o]) { 70 return false; 71 } 72 } 73 } 74 } 75 return true; 76 } 77 78 static bool sameMetaData(const SsrCorr::MetaData* m1, const SsrCorr::MetaData* m2) { 79 if (m1->SSRIOD != m2->SSRIOD || m1->SSRProviderID != m2->SSRProviderID || 80 m1->SSRSolutionID != m2->SSRSolutionID || m1->NumEntries != m2->NumEntries) { 81 return false; 82 } 83 for (unsigned i = 0; i < m1->NumEntries; i++) { 84 if (memcmp(&m1->Entries[i], &m2->Entries[i], sizeof(m1->Entries[i])) != 0) { 85 return false; 86 } 87 } 88 return true; 89 } 90 91 static int nFailed = 0; 92 93 // Decode, print a summary, encode the result again and compare 35 94 static void decode(const char* name, SsrCorr* ssr, const unsigned char* frame, size_t size) { 36 SsrCorr::ClockOrbit* co = new SsrCorr::ClockOrbit(); 37 SsrCorr::CodeBias* cb = new SsrCorr::CodeBias(); 38 SsrCorr::VTEC* v = new SsrCorr::VTEC(); 39 SsrCorr::PhaseBias* pb = new SsrCorr::PhaseBias(); 40 SsrCorr::MetaData* md = new SsrCorr::MetaData(); 41 SsrCorr::Antenna* a = new SsrCorr::Antenna(); 42 int bytesused = 0; 43 44 int irc = ssr->GetSSR(co, cb, v, pb, md, a, (const char*)frame, size, &bytesused); 95 t_target t1; 96 int irc = t1.decode(ssr, frame, size); 45 97 printf(" %-8s irc %3d", name, irc); 46 if (irc >= 0 && v->NumLayers) { 98 if (irc >= 0 && t1.v->NumLayers) { 47 99 double sum = 0.0; 48 100 printf(" VTEC"); 49 for (unsigned l = 0; l < v->NumLayers; l++) { 50 const SsrCorr::VTEC::IonoLayers& layer = v->Layers[l]; 101 for (unsigned l = 0; l < t1.v->NumLayers; l++) { 102 const SsrCorr::VTEC::IonoLayers& layer = t1.v->Layers[l]; 51 103 printf(" %u/%u", layer.Degree, layer.Order); 52 104 for (unsigned d = 0; d <= layer.Degree; d++) { … … 57 109 } 58 110 printf(" sum(C-S) %.3f", sum); 111 112 char buffer[CLOCKORBIT_BUFFERSIZE]; 113 size_t len = ssr->MakeVTEC(t1.v, 0, buffer, sizeof(buffer)); 114 t_target t2; 115 bool ok = len > 0 && t2.decode(ssr, (const unsigned char*)buffer, len) >= 0 && sameVTEC(t1.v, t2.v); 116 printf(" re-encoded: %s", ok ? "identical" : "DIFFERENT"); 117 nFailed += !ok; 59 118 } 60 if (irc >= 0 && md->NumEntries) { 61 printf(" META %u entries, additional data", md->NumEntries); 62 for (unsigned i = 0; i < md->NumEntries; i++) { 63 if (md->Entries[i].additionalParameterIndicator) { 64 printf(" %u:0x%x", md->Entries[i].additionalParameterNumBits, 65 md->Entries[i].additionalParameterData); 119 if (irc >= 0 && t1.md->NumEntries) { 120 printf(" META %u entries, additional data", t1.md->NumEntries); 121 for (unsigned i = 0; i < t1.md->NumEntries; i++) { 122 if (t1.md->Entries[i].additionalParameterIndicator) { 123 printf(" %u:0x%x", t1.md->Entries[i].additionalParameterNumBits, 124 t1.md->Entries[i].additionalParameterData); 66 125 } 126 } 127 SsrCorrRtcmNew* rtcmNew = dynamic_cast<SsrCorrRtcmNew*>(ssr); 128 if (rtcmNew) { 129 char buffer[CLOCKORBIT_BUFFERSIZE]; 130 size_t len = rtcmNew->MakeMetaData(t1.md, buffer, sizeof(buffer)); 131 t_target t2; 132 bool ok = len > 0 && t2.decode(ssr, (const unsigned char*)buffer, len) >= 0 && sameMetaData(t1.md, t2.md); 133 printf(" re-encoded: %s", ok ? "identical" : "DIFFERENT"); 134 nFailed += !ok; 67 135 } 68 136 } 69 137 printf("\n"); 70 71 delete co; delete cb; delete v; delete pb; delete md; delete a;72 138 } 73 139 … … 111 177 ii += len + 6; 112 178 } 179 if (nFailed) { 180 printf("%d decoded messages differ after re-encoding\n", nFailed); 181 return 1; 182 } 113 183 return 0; 114 184 }
Note:
See TracChangeset
for help on using the changeset viewer.
