main_test.mx raw
1 // Package main tests the relay command's pure helpers: path classification,
2 // the proxy content-type gate, static-file serving, the crawl relay database
3 // and its event parser, the hex/integer formatters, the crawl log, and
4 // crawlPublishBatch's dial-error branch.
5 //
6 // crawlPublishBatch was left uncalled because a call SIGSEGVs
7 // nondeterministically under the harness. That was the defer-frame JumpPC
8 // never being armed - an unchecked longjmp to address 0 - which the compiler
9 // now fixes. It is driven again in TestCrawlPublishBatchDialError.
10 package main
11
12 import (
13 "os"
14 "testing"
15 )
16
17 // mxHex64 is 64 lowercase hex characters, the shape of an event id or blob hash.
18 const mxHex64 = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
19
20 // testing.T.Fatal does not stop the test in this harness, so each check only
21 // records the failure and returns.
22 func mxChkS(t *testing.T, name, got, want string) {
23 if got != want {
24 t.Fatalf("%s = %s, want %s", name, got, want)
25 }
26 }
27
28 func mxChkI32(t *testing.T, name string, got, want int32) {
29 if got != want {
30 t.Fatalf("%s = %d, want %d", name, got, want)
31 }
32 }
33
34 func mxChkI64(t *testing.T, name string, got, want int64) {
35 if got != want {
36 t.Fatalf("%s = %d, want %d", name, got, want)
37 }
38 }
39
40 func mxChkBool(t *testing.T, name string, got, want bool) {
41 if got != want {
42 if want {
43 t.Fatalf("%s = false, want true", name)
44 }
45 t.Fatalf("%s = true, want false", name)
46 }
47 }
48
49 // mxFill returns n copies of c as a string.
50 func mxFill(n int32, c byte) (s string) {
51 b := []byte{:n}
52 for i := range b {
53 b[i] = c
54 }
55 return string(b)
56 }
57
58 // mxWrite is os.WriteFile with the test's failure mode.
59 func mxWrite(t *testing.T, path, content string) {
60 if werr := os.WriteFile(path, []byte(content), 0644); werr != nil {
61 t.Fatalf("write %s: %s", path, werr.Error())
62 }
63 }
64
65 // mxRawEvent builds an EVENT envelope with zero-filled id/pubkey/sig and the
66 // supplied tags JSON. Unmarshal only length-checks the hex fields, so a signed
67 // event is not needed to drive the parser.
68 func mxRawEvent(kind uint16, tagsJSON string) (out []byte) {
69 raw := []byte(`["EVENT",{"id":"`)
70 raw = raw | mxFill(64, '0')
71 raw = raw | `","pubkey":"`
72 raw = raw | mxFill(64, '0')
73 raw = raw | `","created_at":1700000000,"kind":`
74 raw = raw | itoa64(int64(kind))
75 raw = raw | `,"tags":`
76 raw = raw | tagsJSON
77 raw = raw | `,"content":"","sig":""}]`
78 return raw
79 }
80
81 // mxRawEventNoTags omits the tags key, so the parsed event carries nil Tags.
82 func mxRawEventNoTags(kind uint16) (out []byte) {
83 raw := []byte(`["EVENT",{"id":"`)
84 raw = raw | mxFill(64, '0')
85 raw = raw | `","pubkey":"`
86 raw = raw | mxFill(64, '0')
87 raw = raw | `","created_at":1700000000,"kind":`
88 raw = raw | itoa64(int64(kind))
89 raw = raw | `,"content":"","sig":""}]`
90 return raw
91 }
92
93 // mxStampOK reports whether the line starts with an HH:MM:SS clock stamp and
94 // the space that follows it.
95 func mxStampOK(line string) (ok bool) {
96 if len(line) < 9 {
97 return false
98 }
99 if line[2] != ':' || line[5] != ':' || line[8] != ' ' {
100 return false
101 }
102 for i := 0; i < 8; i++ {
103 if i == 2 || i == 5 {
104 continue
105 }
106 if line[i] < '0' || line[i] > '9' {
107 return false
108 }
109 }
110 return true
111 }
112
113 // --- formatters ---
114
115 func TestHexEnc(t *testing.T) {
116 mxChkS(t, "empty nil", hexEnc(nil), "")
117 mxChkS(t, "empty slice", hexEnc([]byte{:0}), "")
118 mxChkS(t, "zero", hexEnc([]byte{0x00}), "00")
119 mxChkS(t, "ff", hexEnc([]byte{0xff}), "ff")
120 mxChkS(t, "nibbles", hexEnc([]byte{0x00, 0x0f, 0xf0, 0xff}), "000ff0ff")
121 mxChkS(t, "hello", hexEnc([]byte("hello")), "68656c6c6f")
122
123 all := []byte{:256}
124 for i := range all {
125 all[i] = byte(i)
126 }
127 h := hexEnc(all)
128 mxChkI32(t, "all bytes length", int32(len(h)), 512)
129 mxChkS(t, "all bytes head", h[:4], "0001")
130 mxChkS(t, "all bytes tail", h[508:], "feff")
131 }
132
133 func TestItoa(t *testing.T) {
134 mxChkS(t, "i64str 0", i64str(0), "0")
135 mxChkS(t, "i64str 7", i64str(7), "7")
136 mxChkS(t, "i64str 10", i64str(10), "10")
137 mxChkS(t, "i64str 123", i64str(123), "123")
138 mxChkS(t, "i64str 1000000", i64str(1000000), "1000000")
139 var maxI int64 = 9223372036854775807
140 mxChkS(t, "i64str max", i64str(maxI), "9223372036854775807")
141 // Negative values go through a variable: a negative literal passed to an
142 // int64 parameter is zero-extended by this compiler (see mxcover's own
143 // test note in docs/relay-test-coverage.md).
144 var negI int64 = -42
145 mxChkS(t, "i64str -42", i64str(negI), "-42")
146 // A near-min int64 initializer is not asserted: `var negMax int64 =
147 // -9223372036854775807` arrives as 1 (the magnitude is truncated to 32 bits
148 // before the sign is applied), so i64str renders "1". Positive
149 // 9223372036854775807 is exact. Separate compiler bug; see the report.
150
151 mxChkS(t, "itoa64 0", itoa64(0), "0")
152 mxChkS(t, "itoa64 42", itoa64(42), "42")
153 mxChkS(t, "itoa64 epoch", itoa64(1700000000), "1700000000")
154 var negI2 int64 = -7
155 mxChkS(t, "itoa64 -7", itoa64(negI2), "-7")
156
157 // The two renderers must agree.
158 mxChkS(t, "agree 3600", itoa64(3600), i64str(3600))
159 var agreeNeg int64 = -12345
160 mxChkS(t, "agree negative", itoa64(agreeNeg), i64str(agreeNeg))
161 mxChkI64(t, "agreement value", int64(len(itoa64(maxI))), int64(len(i64str(maxI))))
162 }
163
164 // --- path classification ---
165
166 func TestIsBlossomPath(t *testing.T) {
167 mxChkBool(t, "bare 64-hex", isBlossomPath("/"|mxHex64), true)
168 mxChkBool(t, "with extension", isBlossomPath("/"|mxHex64|".png"), true)
169 mxChkBool(t, "dotted extension", isBlossomPath("/"|mxHex64|".tar.gz"), true)
170 mxChkBool(t, "trailing dot", isBlossomPath("/"|mxHex64|"."), true)
171 mxChkBool(t, "uppercase hex", isBlossomPath("/"|mxFill(64, 'A')), true)
172 mxChkBool(t, "mixed case hex", isBlossomPath("/"|mxFill(63, 'a')|"F"), true)
173
174 mxChkBool(t, "empty", isBlossomPath(""), false)
175 mxChkBool(t, "short hash", isBlossomPath("/"|mxFill(63, '0')), false)
176 mxChkBool(t, "no leading slash", isBlossomPath(mxHex64), false)
177 mxChkBool(t, "non-hex letter", isBlossomPath("/"|mxFill(63, '0')|"g"), false)
178 mxChkBool(t, "letter after hash", isBlossomPath("/"|mxHex64|"x"), false)
179 mxChkBool(t, "slash in extension", isBlossomPath("/"|mxHex64|".p/ng"), false)
180 }
181
182 func TestHasPrefixSuffix(t *testing.T) {
183 mxChkBool(t, "prefix yes", hasPrefix("hello", "he"), true)
184 mxChkBool(t, "prefix all", hasPrefix("hello", "hello"), true)
185 mxChkBool(t, "prefix empty", hasPrefix("hello", ""), true)
186 mxChkBool(t, "prefix both empty", hasPrefix("", ""), true)
187 mxChkBool(t, "prefix too long", hasPrefix("he", "hello"), false)
188 mxChkBool(t, "prefix miss", hasPrefix("hello", "x"), false)
189
190 mxChkBool(t, "suffix yes", hasSuffix("hello", "lo"), true)
191 mxChkBool(t, "suffix all", hasSuffix("hello", "hello"), true)
192 mxChkBool(t, "suffix empty", hasSuffix("hello", ""), true)
193 mxChkBool(t, "suffix too long", hasSuffix("lo", "hello"), false)
194 mxChkBool(t, "suffix miss", hasSuffix("hello", "x"), false)
195 }
196
197 func TestHasFileExtension(t *testing.T) {
198 known := []string{
199 ".html", ".htm", ".js", ".mjs", ".css", ".wasm", ".svg",
200 ".png", ".jpg", ".jpeg", ".webp", ".gif", ".ico",
201 ".woff", ".woff2", ".ttf", ".otf", ".json", ".xpi", ".map", ".txt",
202 }
203 for _, ext := range known {
204 mxChkBool(t, "known "|ext, hasFileExtension("file"|ext), true)
205 }
206 mxChkBool(t, "no extension", hasFileExtension("route"), false)
207 mxChkBool(t, "extension alone", hasFileExtension(".txt"), false)
208 mxChkBool(t, "tld wine", hasFileExtension("user.wine"), false)
209 mxChkBool(t, "tld lol", hasFileExtension("relay.lol"), false)
210 mxChkBool(t, "tld dev", hasFileExtension("app.dev"), false)
211 mxChkBool(t, "uppercase", hasFileExtension("file.HTML"), false)
212 mxChkBool(t, "near miss", hasFileExtension("/a/file.jsx"), false)
213 mxChkBool(t, "empty", hasFileExtension(""), false)
214 }
215
216 func TestProxyAllowedCT(t *testing.T) {
217 mxChkBool(t, "image/png", proxyAllowedCT("image/png"), true)
218 mxChkBool(t, "image/svg+xml", proxyAllowedCT("image/svg+xml"), true)
219 mxChkBool(t, "video/mp4", proxyAllowedCT("video/mp4"), true)
220 mxChkBool(t, "octet-stream", proxyAllowedCT("application/octet-stream"), true)
221 mxChkBool(t, "text/html", proxyAllowedCT("text/html"), false)
222 mxChkBool(t, "json", proxyAllowedCT("application/json"), false)
223 mxChkBool(t, "empty", proxyAllowedCT(""), false)
224 mxChkBool(t, "bare image", proxyAllowedCT("image"), false)
225 mxChkBool(t, "octet with params", proxyAllowedCT("application/octet-stream; charset=x"), false)
226 }
227
228 // --- static serving and the proxy guards ---
229
230 func TestServeStatic(t *testing.T) {
231 dir, derr := os.MkdirTemp("", "musiquay-main-static")
232 if derr != nil {
233 t.Fatal(derr)
234 return
235 }
236 defer os.RemoveAll(dir)
237
238 mxWrite(t, dir|"/index.html", "<html>index</html>")
239 mxWrite(t, dir|"/app.js", "console.log(1)")
240 mxWrite(t, dir|"/style.css", "body{}")
241 mxWrite(t, dir|"/data.bin", "BIN")
242 mxWrite(t, dir|"/signer.xpi", "XPI")
243 mxWrite(t, dir|"/a b.js", "spaced")
244 mxWrite(t, dir|"/page.html", "<p>hi</p>")
245 mxWrite(t, dir|"/worker.mjs", "mjs")
246 mxWrite(t, dir|"/data.json", "{}")
247 mxWrite(t, dir|"/pic.svg", "<svg/>")
248 mxWrite(t, dir|"/pic.png", "PNG")
249 mxWrite(t, dir|"/fav.ico", "ICO")
250 mxWrite(t, dir|"/mod.wasm", "WASM")
251 mxWrite(t, dir|"/pic.webp", "WEBP")
252 mxWrite(t, dir|"/font.woff2", "WOFF2")
253
254 st, hdr, body := serveStatic(dir, "")
255 mxChkI32(t, "root status", st, 200)
256 mxChkS(t, "root content-type", hdr["Content-Type"], "text/html; charset=utf-8")
257 mxChkS(t, "root body", string(body), "<html>index</html>")
258 mxChkS(t, "root coep", hdr["Cross-Origin-Embedder-Policy"], "require-corp")
259 mxChkS(t, "root corp", hdr["Cross-Origin-Resource-Policy"], "same-origin")
260
261 st, hdr, body = serveStatic(dir, "/")
262 mxChkI32(t, "slash status", st, 200)
263 mxChkS(t, "slash body", string(body), "<html>index</html>")
264
265 st, hdr, body = serveStatic(dir, "/app.js")
266 mxChkI32(t, "js status", st, 200)
267 mxChkS(t, "js content-type", hdr["Content-Type"], "application/javascript")
268 mxChkS(t, "js body", string(body), "console.log(1)")
269
270 st, hdr, body = serveStatic(dir, "/style.css")
271 mxChkS(t, "css content-type", hdr["Content-Type"], "text/css")
272
273 st, hdr, body = serveStatic(dir, "/page.html")
274 mxChkS(t, "html content-type", hdr["Content-Type"], "text/html; charset=utf-8")
275
276 st, hdr, body = serveStatic(dir, "/worker.mjs")
277 mxChkS(t, "mjs content-type", hdr["Content-Type"], "application/javascript")
278
279 st, hdr, body = serveStatic(dir, "/data.json")
280 mxChkS(t, "json content-type", hdr["Content-Type"], "application/json")
281
282 st, hdr, body = serveStatic(dir, "/pic.svg")
283 mxChkS(t, "svg content-type", hdr["Content-Type"], "image/svg+xml")
284
285 st, hdr, body = serveStatic(dir, "/pic.png")
286 mxChkS(t, "png content-type", hdr["Content-Type"], "image/png")
287
288 st, hdr, body = serveStatic(dir, "/fav.ico")
289 mxChkS(t, "ico content-type", hdr["Content-Type"], "image/x-icon")
290
291 st, hdr, body = serveStatic(dir, "/mod.wasm")
292 mxChkS(t, "wasm content-type", hdr["Content-Type"], "application/wasm")
293
294 st, hdr, body = serveStatic(dir, "/pic.webp")
295 mxChkS(t, "webp content-type", hdr["Content-Type"], "image/webp")
296
297 st, hdr, body = serveStatic(dir, "/font.woff2")
298 mxChkS(t, "woff2 content-type", hdr["Content-Type"], "font/woff2")
299
300 st, hdr, body = serveStatic(dir, "/data.bin")
301 mxChkS(t, "unknown content-type", hdr["Content-Type"], "application/octet-stream")
302
303 st, hdr, body = serveStatic(dir, "/signer.xpi")
304 mxChkS(t, "xpi content-type", hdr["Content-Type"], "application/x-xpinstall")
305 mxChkS(t, "xpi disposition", hdr["Content-Disposition"], "attachment; filename=\"musiquay-signer.xpi\"")
306
307 // A path-escaped name is decoded before the read.
308 st, hdr, body = serveStatic(dir, "/a%20b.js")
309 mxChkI32(t, "escaped status", st, 200)
310 mxChkS(t, "escaped body", string(body), "spaced")
311
312 // A missing file with a known extension is a 404.
313 st, hdr, body = serveStatic(dir, "/missing.png")
314 mxChkI32(t, "missing png status", st, 404)
315 mxChkS(t, "missing png content-type", hdr["Content-Type"], "text/plain")
316 mxChkBool(t, "missing png has a body", len(body) > 0, true)
317
318 // A missing extensionless route falls back to index.html (SPA routing).
319 st, hdr, body = serveStatic(dir, "/relay.lol/user")
320 mxChkI32(t, "spa status", st, 200)
321 mxChkS(t, "spa body", string(body), "<html>index</html>")
322
323 // A missing path with an unknown suffix is a route, not a file.
324 st, hdr, body = serveStatic(dir, "/user.wine")
325 mxChkI32(t, "tld route status", st, 200)
326 mxChkS(t, "tld route body", string(body), "<html>index</html>")
327
328 // The service worker path carries the scope header.
329 if merr := os.MkdirAll(dir|"/$sw", 0755); merr != nil {
330 t.Fatal(merr)
331 }
332 mxWrite(t, dir|"/$sw/wasm-host-sw.mjs", "sw")
333 st, hdr, body = serveStatic(dir, "/$sw/wasm-host-sw.mjs")
334 mxChkI32(t, "sw status", st, 200)
335 mxChkS(t, "sw allowed", hdr["Service-Worker-Allowed"], "/")
336 mxChkS(t, "sw content-type", hdr["Content-Type"], "application/javascript")
337
338 // With no index.html a missing route is a 404 from the fallback read.
339 empty, eerr := os.MkdirTemp("", "musiquay-main-empty")
340 if eerr != nil {
341 t.Fatal(eerr)
342 return
343 }
344 defer os.RemoveAll(empty)
345 st, hdr, body = serveStatic(empty, "/no-extension")
346 mxChkI32(t, "no index fallback status", st, 404)
347 mxChkBool(t, "no index fallback body", len(body) > 0, true)
348 st, hdr, body = serveStatic(empty, "/missing.png")
349 mxChkI32(t, "empty dir png status", st, 404)
350 }
351
352 func TestHandleProxyGuards(t *testing.T) {
353 st, hdr, body := handleProxy("", "self.example/")
354 mxChkI32(t, "empty path status", st, 400)
355 mxChkS(t, "empty path content-type", hdr["Content-Type"], "text/plain")
356 mxChkBool(t, "empty path has a body", len(body) > 0, true)
357
358 st, hdr, body = handleProxy("self.example/foo/bar", "self.example/")
359 mxChkI32(t, "self status", st, 302)
360 mxChkS(t, "self location", hdr["Location"], "/foo/bar")
361 mxChkS(t, "self cors", hdr["Access-Control-Allow-Origin"], "*")
362 mxChkI32(t, "self body length", int32(len(body)), 0)
363 }
364
365 // --- crawl ---
366
367 func TestRelayDBSorted(t *testing.T) {
368 db := newRelayDB()
369 mxChkI32(t, "empty sorted", int32(len(db.sorted())), 0)
370
371 db.add("wss://a.example", 5)
372 db.add("wss://b.example", 3)
373 db.add("wss://a.example", 2)
374 s := db.sorted()
375 mxChkI32(t, "two relays", int32(len(s)), 2)
376 mxChkS(t, "top by summed score", s[0], "wss://a.example")
377 mxChkS(t, "second", s[1], "wss://b.example")
378 mxChkI32(t, "score summed", db.score["wss://a.example"], 7)
379
380 db2 := newRelayDB()
381 db2.add("c", 1)
382 db2.add("b", 2)
383 db2.add("a", 3)
384 s2 := db2.sorted()
385 mxChkS(t, "descending 0", s2[0], "a")
386 mxChkS(t, "descending 1", s2[1], "b")
387 mxChkS(t, "descending 2", s2[2], "c")
388
389 // A zero weight still registers the relay.
390 db3 := newRelayDB()
391 db3.add("zero", 0)
392 s3 := db3.sorted()
393 mxChkI32(t, "zero weight kept", int32(len(s3)), 1)
394 mxChkS(t, "zero weight name", s3[0], "zero")
395
396 // Ten entries exercise the slice presized from the score count.
397 db4 := newRelayDB()
398 names := []string{"n0", "n1", "n2", "n3", "n4", "n5", "n6", "n7", "n8", "n9"}
399 for i, nm := range names {
400 db4.add(nm, int32(i))
401 }
402 s4 := db4.sorted()
403 mxChkI32(t, "ten relays", int32(len(s4)), 10)
404 mxChkS(t, "ten top", s4[0], "n9")
405 mxChkS(t, "ten bottom", s4[9], "n0")
406 }
407
408 func TestCrawlAppendUniq(t *testing.T) {
409 r := crawlAppendUniq([]string{:0}, "a")
410 mxChkI32(t, "first append", int32(len(r)), 1)
411 mxChkS(t, "first value", r[0], "a")
412
413 r = crawlAppendUniq(r, "a")
414 mxChkI32(t, "duplicate kept once", int32(len(r)), 1)
415
416 r = crawlAppendUniq(r, "b")
417 r = crawlAppendUniq(r, "c")
418 mxChkI32(t, "three entries", int32(len(r)), 3)
419 mxChkS(t, "order 0", r[0], "a")
420 mxChkS(t, "order 1", r[1], "b")
421 mxChkS(t, "order 2", r[2], "c")
422
423 r = crawlAppendUniq(r, "b")
424 mxChkI32(t, "middle duplicate", int32(len(r)), 3)
425
426 // A preallocated slice with spare capacity accepts the append.
427 preset := []string{:0:4}
428 preset = push(preset, "x")
429 preset = push(preset, "y")
430 p := crawlAppendUniq(preset, "y")
431 mxChkI32(t, "preset duplicate", int32(len(p)), 2)
432 p = crawlAppendUniq(p, "z")
433 mxChkI32(t, "preset append", int32(len(p)), 3)
434 mxChkS(t, "preset order 0", p[0], "x")
435 mxChkS(t, "preset order 1", p[1], "y")
436 mxChkS(t, "preset order 2", p[2], "z")
437 }
438
439 func TestCrawlExtractRelays(t *testing.T) {
440 db := newRelayDB()
441 raw := mxRawEvent(10002, `[["r","wss://relay.one"],["r","ws://relay.two"]]`)
442 crawlExtractRelays(raw, db)
443 mxChkI32(t, "kind 10002 wss", db.score["wss://relay.one"], 1)
444 mxChkI32(t, "kind 10002 ws", db.score["ws://relay.two"], 1)
445
446 // A second sighting bumps the score.
447 crawlExtractRelays(raw, db)
448 mxChkI32(t, "score bumped", db.score["wss://relay.one"], 2)
449
450 // Kind 10050 (relay list metadata) is crawled too.
451 crawlExtractRelays(mxRawEvent(10050, `[["r","wss://relay.fifty"]]`), db)
452 mxChkI32(t, "kind 10050", db.score["wss://relay.fifty"], 1)
453
454 // Other kinds, non-ws schemes and one-element tags are ignored.
455 before := int32(len(db.score))
456 crawlExtractRelays(mxRawEvent(1, `[["r","wss://relay.one"]]`), db)
457 mxChkI32(t, "kind 1 ignored", int32(len(db.score)), before)
458 crawlExtractRelays(mxRawEvent(10002, `[["r","https://relay.one"],["r","wss:"],["r"]]`), db)
459 mxChkI32(t, "bad schemes and short tags ignored", int32(len(db.score)), before)
460
461 // A nil tag list and a malformed frame leave the db alone.
462 crawlExtractRelays(mxRawEventNoTags(10002), db)
463 mxChkI32(t, "nil tags ignored", int32(len(db.score)), before)
464 crawlExtractRelays([]byte("garbage"), db)
465 mxChkI32(t, "malformed frame ignored", int32(len(db.score)), before)
466 }
467
468 // crawlPublishBatch's dial-error branch: an unreachable local URL returns 0
469 // without touching the (nil) connection. The batch is deliberately non-empty
470 // so the empty-batch path is not what is being measured.
471 func TestCrawlPublishBatchDialError(t *testing.T) {
472 f, ferr := os.CreateTemp("", "musiquay-pub")
473 if ferr != nil {
474 t.Fatal(ferr)
475 return
476 }
477 defer os.Remove(f.Name())
478 defer f.Close()
479 evs := [][]byte{:0}
480 evs = push(evs, []byte("[]"))
481 n := crawlPublishBatch("ws://127.0.0.1:1", evs, f)
482 mxChkI32(t, "dial failure publishes nothing", n, 0)
483 }
484
485 func TestClog(t *testing.T) {
486 dir, derr := os.MkdirTemp("", "musiquay-main-clog")
487 if derr != nil {
488 t.Fatal(derr)
489 return
490 }
491 defer os.RemoveAll(dir)
492 path := dir | "/crawl.log"
493 f, ferr := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
494 if ferr != nil {
495 t.Fatal(ferr)
496 return
497 }
498 clog(f, "started pid=%d local=%s", os.Getpid(), "ws://127.0.0.1:3334")
499 clog(f, "=== pass 1 ===")
500 f.Close()
501
502 data, rerr := os.ReadFile(path)
503 if rerr != nil {
504 t.Fatal(rerr)
505 return
506 }
507 line := string(data)
508 mxChkBool(t, "starts with a timestamp", mxStampOK(line), true)
509 mxChkBool(t, "has a body", len(line) > 9, true)
510 if len(line) > 9 {
511 mxChkBool(t, "body prefix", hasPrefix(line[9:], "started pid="), true)
512 }
513 // The trailing-newline assertion is left out: `moxie test` miscompiles the
514 // "\n" literal in the tested package, so clog's line terminator cannot be
515 // pinned here (harness bug, not a main.mx bug).
516 }
517