-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathMakefile
More file actions
1145 lines (1105 loc) · 55.5 KB
/
Copy pathMakefile
File metadata and controls
1145 lines (1105 loc) · 55.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
# PinPointCapture — build, test and deploy.
#
# The .xcodeproj is generated by XcodeGen from project.yml and is gitignored.
# Run `make gen` after pulling, after adding files, or whenever the project
# looks stale. Never edit the project file by hand.
SCHEME := PinPointCapture
PROJECT := PinPointCapture.xcodeproj
APP := PinPointCapture.app
BUNDLE_ID := org.pinpointstudio.capture
CONFIG ?= Debug
DERIVED := build
# ⚠ Resolved at run time, not hardcoded. The installed simulator set changes
# with every Xcode release — an iOS 27 runtime ships iPhone 17s and no 16.
SIM_NAME ?= $(shell xcrun simctl list devices available 2>/dev/null \
| grep -oE 'iPhone [0-9]+( Pro)?' | head -1)
SIM_DEST ?= generic/platform=iOS Simulator
TEST_DEST ?= platform=iOS Simulator,name=$(SIM_NAME)
DEV_DEST ?= generic/platform=iOS
XCB := xcbeautify --quieter
DEVICE_APP := $(DERIVED)/Build/Products/$(CONFIG)-iphoneos/$(APP)
# ⚠ **Every build is capped at three jobs, and it is not a preference.** This is
# a 16 GB machine and an unbounded `xcodebuild` alongside a `swift build` took it
# down on 22 August 2026. `-jobs`/`-j` here rather than in each recipe so a new
# target cannot forget.
JOBS := 3
# ⛔ **A WALL-CLOCK CEILING ON EVERY UNATTENDED xcodebuild.** On 24 August 2026 a
# hung test held `xcodebuild` for 25 minutes at 0% CPU. Nothing killed it:
# `-default-test-execution-time-allowance 120` and `-maximum-test-execution-time-
# allowance 300` are both set below and **neither fired** — they do not apply to a
# hang inside a Swift Testing async case, which is precisely what hung. It was
# noticed only because someone happened to be watching, and **a hang reads as
# "still running", so nothing about it looks wrong.**
#
# ⚠ macOS ships no `timeout(1)`. `perl -e 'alarm shift; exec @ARGV'` is the
# portable equivalent: the alarm survives `exec`, so the timer belongs to
# xcodebuild itself rather than to a wrapper it could outlive. Exit status is
# 142 (128 + SIGALRM).
#
# Sizing, from the run of 2026-08-24 (`Test-PinPointCapture-2026.08.24_17-56-33`):
# 74 cases, **median 0.001s**, slowest **14.0s**, whole suite ~55s. The ceiling is
# an order of magnitude above that on purpose — it is a backstop against a hang,
# not a performance budget, and it must never fire on a slow-but-honest machine.
# ⛔ **`-collect-test-diagnostics never` ON EVERY TEST RUN, AND IT IS THE FIX FOR
# A TEN-MINUTE STALL.** Measured 3 September 2026: a suite that PASSED in 56.8s
# held xcodebuild until 661.3s elapsed, and the log says why —
#
# IDETestOperationsObserverDebug: Failure collecting diagnostics from
# simulator: Timed out after 600.0 seconds while waiting for a response
# from the invoked process
#
# The default is `on-failure`, and swift-testing's `withKnownIssue` counts as a
# failure for this purpose: the ten conformance/interop cases that SKIP
# themselves when no `ppcp-sim`/`PPCP_INTEROP_HOST` is in the environment are
# recorded as issues, so a plain `make test` triggers a sysdiagnose collection
# EVERY TIME. The simulator never answers it, the collector waits its full
# hard-coded 600s, gives up, and prints `** TEST SUCCEEDED **`. Nothing is
# collected — the ten minutes buy a timeout message.
#
# ⚠ This is why the run always looked like a hang and never was, and why raising
# TEST_TIMEOUT_S "fixed" it: the stall is a fixed 600s that the guard was racing.
# Do not remove it to get diagnostics on a real failure — this collector has
# never once produced any. The xcresult bundle under $(DERIVED)/Logs/Test is
# where a failure is actually read.
NO_DIAG := -collect-test-diagnostics never
GUARD := /usr/bin/perl -e 'alarm shift; exec @ARGV'
GUARD_STATUS := 142
TEST_TIMEOUT_S ?= 600
BUILD_TIMEOUT_S ?= 600
# `libppcp`'s conformance tools. A sibling checkout during co-development.
LIBPPCP ?= ../libppcp
PPCP_SIM ?= $(LIBPPCP)/build/dev/tools/ppcp-sim/ppcp-sim
PPCP_CONFORM ?= $(LIBPPCP)/build/dev/tools/ppcp-conform/ppcp-conform
# ⛔ **The relay is the ONLY thing that can see trap 2** — `bs_accept` sent after
# `pk_i` arrives changes nothing on the wire and passes every static test in this
# repository. `--probe order-acceptor` withholds `bs_reveal` and checks that
# `pk_a` had already arrived, which is RT-20b(ii) for an acceptor.
# ⚠ It needs `openssl` on PATH: §11.11 keeps X25519 out of libppcp, so the relay
# supplies its own across the same boundary, over a pipe. Its absence must read
# as a MISSING TOOL and never as a handshake failure.
PPCP_RELAY ?= $(LIBPPCP)/build/dev/tools/ppcp-relay/ppcp-relay
# ⚠ Fixed, because the relay DIALS this application: the port has to be known
# before the app is running. `BootstrapAdvertiser` binds port 0 everywhere else.
RV6_PORT ?= 47799
# How long to wait for the simulator to boot, install and bind the window.
RV6_WARMUP_S ?= 90
# ⚠ **Two axes, and keeping them apart is the point of `tools/scenarios`.** What
# the peer IS is a declaration file; what it DOES is a named scenario. "A host
# that never answers a Candidate" is `SCENARIO=silent-host` over the *reference*
# host declaration — deriving the file name from the scenario looked tidy and
# broke the moment the two differed.
SCENARIO ?= reference-host
DECL ?= reference-host
SCENARIO_DECL ?= $(LIBPPCP)/tools/scenarios/$(DECL).json
# ⚠ **Which `CONF` §5 interoperability row this run is.** `ppcp-sim` serves one
# link, so a suite where several tests dialled the same listener would have all
# but one measuring a peer that had already finished. `ROW` picks exactly one;
# `d9` (the default) is the CT-S5 device row that has always been there.
ROW ?= d9
# What `ppcp-sim` is told to expect of ITSELF, on top of seeing no violation.
# A row's assertions live on both sides: this is the far end's half.
EXPECT ?= violations=0
# ⚠ Long enough for a simulator to install and run, and no longer — the target
# `wait`s for ppcp-sim so it can read the exit code its `--expect` produces, so
# this number is also how long `make conform` takes after the test has passed.
# The device side's own deadline is `ConformanceHarness.run(seconds:)`.
CONFORM_RUN_MS ?= 45000
# ⛔ **The claim** (`CONF` 1a): a conformance run without a profile set is not a
# claim. This is the set `docs/conformance/ppcp-conformance.md` §1 states, and the two must
# not drift — a row that passes against a set this application does not claim is
# measuring somebody else.
CONFORM_PROFILES ?= core,capture,detect,mint,live,offline,markup,actuate
CONFORM_OUT ?= docs/conformance
# IOP-3 / IOP-10 — the bundles PinPointStudio imports. Small, and checked in.
BUNDLE_OUT ?= docs/conformance/bundles
# How long the simulator gets to boot, install and reach the dial loop before
# `ppcp-conform` starts binding ports. ⚠ Generous on purpose: a row lost to a
# slow boot is a FAILED row, and there is no way to tell that apart from a real
# refusal afterwards.
CONFORM_WARMUP_S ?= 75
COMMA := ,
.PHONY: integration-device pull-diags all gen build build-device _udid _udid_paired test test-core test-app conform conform-sim conform-tool conform-iop rv6 _rv6_run interop read-bundle pull-bundles device deploy lint browse clean help
# ⚠ **Two instruments, one target.** `make conform` runs `ppcp-conform`, which is
# what fills the matrix column (plan A11). `make conform SCENARIO=<name>` keeps
# the direct `ppcp-sim` path, which is how a single scenario is driven while
# debugging one. `$(origin ...)` is what tells an explicit SCENARIO from the
# default sitting in this file.
ifeq ($(origin SCENARIO),command line)
conform: conform-sim
else
conform: conform-tool
endif
all: build
help:
@echo "gen regenerate $(PROJECT) from project.yml"
@echo "build build for the simulator"
@echo "build-device build for a physical device (needs signing)"
@echo "test run the unit tests"
@echo "rv6 RT-20b: ppcp-relay against this app's RV-6 acceptor"
@echo "conform ppcp-conform drives the device (D9); SCENARIO=<n> for one ppcp-sim row"
@echo " CONF §5 rows: make conform ROW=iop1|iop2 SCENARIO=... DECL=..."
@echo "pull-bundles copy the bundles an IOP-1 run wrote out of the simulator container"
@echo "read-bundle read a bundle another implementation wrote: FILE=<path.ppcpbndl>"
@echo "interop wave 2: dial the REAL PinPointStudio TLS listeners (HOST=, HOST2=, PSK=, IDENTITY=)"
@echo "device list connected devices"
@echo "deploy gen + build-device + install + launch on a connected device"
@echo "lint run swiftlint"
@echo "clean remove generated project and build products"
gen:
@xcodegen generate --quiet
@echo "generated $(PROJECT)"
$(PROJECT):
@$(MAKE) --no-print-directory gen
# ⚠ Depends on `gen`, not on $(PROJECT). project.yml globs Sources/, so adding a
# new file leaves project.yml untouched and a timestamp rule would rebuild a
# stale project that omits it — surfacing as "cannot find X in scope".
# xcodegen takes well under a second; correctness is worth more than that.
build: gen
set -o pipefail && $(GUARD) $(BUILD_TIMEOUT_S) xcodebuild build \
-project $(PROJECT) \
-scheme $(SCHEME) \
-configuration $(CONFIG) \
-destination '$(SIM_DEST)' \
-derivedDataPath $(DERIVED) \
-jobs $(JOBS) \
| $(XCB)
# ⚠ Builds against the CONNECTED device by UDID, not against a generic iOS
# destination. Automatic signing has to name a specific device to include it in a
# provisioning profile; with `generic/platform=iOS` it cannot, and fails with the
# misleading "Your team has no devices" rather than naming the phone.
# Override with: make build-device UDID=<udid>
build-device: gen
@set -e; \
udid="$(UDID)"; \
if [ -z "$$udid" ]; then udid=$$($(MAKE) --no-print-directory _udid); fi; \
if [ -z "$$udid" ]; then \
echo "make build-device: no paired PHYSICAL device."; \
paired=$$($(MAKE) --no-print-directory _udid_paired); \
if [ -n "$$paired" ]; then \
echo " seen but not usable: $$paired"; \
echo " plug it in, unlock it, and trust this Mac."; \
fi; \
echo " ⛔ a simulator is NOT a substitute here — see the _udid note."; \
exit 1; \
fi; \
echo "device UDID: $$udid"; \
set -o pipefail && xcodebuild build \
-project $(PROJECT) \
-scheme $(SCHEME) \
-configuration $(CONFIG) \
-destination "id=$$udid" \
-derivedDataPath $(DERIVED) \
-jobs $(JOBS) \
-allowProvisioningUpdates \
| $(XCB)
# Resolve the UDID of the first connected PHYSICAL device. Prints nothing if none.
#
# ⛔ **`devicectl list devices` lists simulators too, and they report
# `tunnelState: connected` while a real phone sitting on the desk reports
# `disconnected`.** Filtering on tunnelState alone therefore returns a SIMULATOR
# udid — which `xcodebuild -destination id=...` accepts happily, so `make
# build-device` and `make deploy` succeed against a simulator and every "device
# run" measures a machine with no camera. Found 24 Aug 2026 while setting up
# E1.1's device run; it had been silently true since this target was written.
#
# ⚠ **Filter on `reality` and `pairingState`, NOT on `tunnelState`.** The tunnel
# to a wired device is established per-operation and torn down when idle, so
# `list devices` reports `tunnelState: disconnected` for a phone that is plugged
# in, unlocked and perfectly usable — `devicectl device info details` on the same
# device brings the tunnel up and reports `connected` a second later. Filtering on
# it rejected the phone all of 24 August, which is the second bug in this one
# resolver in a day: it first returned a SIMULATOR, and then nothing at all.
_udid:
@tmp=$$(mktemp -t ppcp-devices); \
xcrun devicectl list devices --json-output "$$tmp" >/dev/null 2>&1 || true; \
python3 -c 'import json,sys;\
d=json.load(open(sys.argv[1])).get("result",{}).get("devices",[]);\
p=[x for x in d if x.get("hardwareProperties",{}).get("reality")=="physical"\
and x.get("connectionProperties",{}).get("pairingState")=="paired"];\
c=[x for x in p if x.get("connectionProperties",{}).get("tunnelState")=="connected"];\
print((c or p)[0]["hardwareProperties"]["udid"] if p else "")' "$$tmp" 2>/dev/null || true; \
rm -f "$$tmp"
# Physical devices that are paired but NOT connected, for a useful error message.
_udid_paired:
@tmp=$$(mktemp -t ppcp-devices); \
xcrun devicectl list devices --json-output "$$tmp" >/dev/null 2>&1 || true; \
python3 -c 'import json,sys;\
d=json.load(open(sys.argv[1])).get("result",{}).get("devices",[]);\
p=[x for x in d if x.get("hardwareProperties",{}).get("reality")=="physical"];\
print("; ".join(x.get("deviceProperties",{}).get("name","?")+" ("+x.get("connectionProperties",{}).get("tunnelState","?")+")" for x in p))' "$$tmp" 2>/dev/null || true; \
rm -f "$$tmp"
# Everything. Core first: it is the fast one, and if the layer seam is broken
# there is no point booting a simulator to find out.
test: test-core test-app
# ⚠ The one to reach for. Runs natively on the host in milliseconds — no
# simulator, no runtime download, no Xcode. Includes the layer purity gate that
# fails the build if Core imports a platform framework (REQ-PORT-3).
test-core:
@cd Packages/Core && swift test -j $(JOBS)
test-app: gen
@if ! xcrun simctl list runtimes 2>/dev/null | grep -q 'iOS'; then \
echo "make test-app: no iOS simulator runtime is installed."; \
echo " Run: xcodebuild -downloadPlatform iOS"; \
echo " (make test-core needs no simulator and covers the Core layer.)"; \
exit 1; \
fi
@if [ -z "$(SIM_NAME)" ]; then \
echo "make test-app: no available iPhone simulator found."; exit 1; \
fi
@echo "simulator: $(SIM_NAME)"
@# ⚠ `↳` is in the filter because swift-testing prefixes the CONTINUATION
@# lines of a multi-line assertion message with it. Without it the failure
@# name survives and the transcript attached to it does not — which is
@# exactly what was thrown away the one time it was wanted (D9, CT-S4 (6)).
@# ⚠ NOT piped through `xcbeautify --quieter`, which swallows swift-testing's
@# output entirely and leaves only XCTest's "Executed 0 tests" summary — so a
@# passing suite looks like an empty one. Filter to the result lines instead.
@# ⚠ The two allowances below are kept because they catch a SLOW test, but
@# they did NOT catch a HUNG one on 2026-08-24 — $(GUARD) is what does.
@# ⛔ **OUTPUT GOES TO A FILE AND IS FILTERED AFTERWARDS, NOT PIPED.**
@# `xcodebuild test | grep` does not return when the tests finish: the
@# simulator and `testmanagerd` inherit xcodebuild's stdout, so the pipe
@# never reaches EOF while a surviving child still holds the descriptor —
@# `grep` waits on a writer that has stopped writing. The suite completes,
@# the results are written, and the command hangs until something kills it.
@# Diagnosed 27 Aug after three wrong explanations (a slow build, a slow
@# test, an editing race); the tell is that killing it prints the whole
@# transcript at once, which a genuinely stuck run cannot do.
@# ⚠ xcodebuild's status is written to a FILE, not read from the pipeline.
@# `set -o pipefail` reports the RIGHTMOST non-zero status, and `grep` exits 1
@# when a killed xcodebuild printed nothing to match — so the timeout's 142 is
@# masked by grep's 1 and a hang reports itself as "no test output". Measured
@# while adding this guard, which is exactly the class of bug it exists for.
@set -o pipefail; \
mkdir -p $(DERIVED); rm -f $(DERIVED)/.xcodebuild-status; \
log=$(DERIVED)/.test-app.log; \
$(GUARD) $(TEST_TIMEOUT_S) xcodebuild test $(NO_DIAG) \
-project $(PROJECT) \
-scheme $(SCHEME) \
-configuration $(CONFIG) \
-destination '$(TEST_DEST)' \
-derivedDataPath $(DERIVED) \
-jobs $(JOBS) \
-default-test-execution-time-allowance 120 \
-maximum-test-execution-time-allowance 300 \
>"$$log" 2>&1 </dev/null; \
echo $$? > $(DERIVED)/.xcodebuild-status; \
grep -E '^(◇|✔|✘|↳)|error:|Test run|\*\* TEST' "$$log" || true; \
status=$$(cat $(DERIVED)/.xcodebuild-status 2>/dev/null || echo 1); \
if [ $$status -eq $(GUARD_STATUS) ]; then \
echo ""; \
echo "make test-app: TIMED OUT after $(TEST_TIMEOUT_S)s and was killed."; \
echo " This suite runs in ~55s (74 cases, median 1ms, slowest 14s), so this"; \
echo " is a hang, not a slow machine. The last test named above is where it"; \
echo " stopped — a hang prints its start line and never its result."; \
echo " Raise the ceiling with: make test-app TEST_TIMEOUT_S=<seconds>"; \
exit 1; \
elif [ $$status -ne 0 ]; then \
echo "make test-app: no test output — see the xcresult bundle"; \
exit 1; \
fi
# D9 — the device peer driven by a counterpart this repository did not write.
#
# ⚠ `ppcp-conform` (libppcp L14) is what this will call once it exists; until
# then it drives `ppcp-sim` directly, which is the same transport and the same
# refusals. The target switches on its own when the binary appears.
#
# ⛔ The simulator peer is started BEFORE the test and killed after, whatever
# happens: a listener left bound holds the port and the next run fails for a
# reason that has nothing to do with conformance.
# ⛔ **RT-20b — the relay against this application's acceptor.** Two runs, and the
# first one is the one that matters.
#
# 1. `--probe order-acceptor` ⛔ 11.5c. Withholds `bs_reveal` and checks that
# `pk_a` had ALREADY arrived — that this peer committed BLIND. Trap 2 is
# invisible on the wire and no static test in this repository can see it.
# 2. `--probe decline` RT-20b(iii)/(iv). The relay reaches the digits
# and refuses; neither end may pair (11.5g), and a SECOND dial must find no
# window (3.7b, 11.9b). ⛔ (iv) had never been run against anything before
# this application — team L withheld it rather than manufacture a green
# against a stand-in that has no window to reopen. So a red here is as
# likely to be the probe as the peer, and is reported rather than designed
# around.
# 3. `--peer initiator` an honest counterpart, so a full five-frame
# exchange completes. ⚠ Runs with PPCP_RV6_MODE=pair, which makes the
# harness affirm its own comparison in software — the one thing 11.1d
# forbids a real peer to do. It is evidence the exchange RUNS, not that it
# authenticates.
#
# ⛔ **`--selftest` FIRST, and it is not a formality.** Its sixth row is a
# negative control: the same probe against a stand-in built to carry trap 2 must
# FAIL, and does. Without it the ordering probe is an untested test.
#
# ⚠ **The direction is the opposite of `make conform`.** There the host listens
# and the device dials; here the device listens and the relay dials. So the test
# runs in the BACKGROUND and the relay starts once `lsof` shows the port bound —
# `rl_connect` tries once with a 5-second timeout and does not retry, and a
# loopback port with nothing on it refuses immediately rather than waiting.
# ⚠ `lsof` rather than a probe dial: a dial would consume one of the acceptor's
# bounded pending slots and put a connection through 11.3c for no reason.
rv6: gen
@if [ ! -x "$(PPCP_RELAY)" ]; then \
echo "make rv6: no ppcp-relay at $(PPCP_RELAY)"; \
echo " Build it: cmake --build $(LIBPPCP)/build/dev --target ppcp-relay"; \
exit 1; \
fi
@command -v openssl >/dev/null 2>&1 || { \
echo "make rv6: openssl is not on PATH."; \
echo " ⛔ This is a MISSING TOOL, not a handshake failure — RV 11.11"; \
echo " keeps X25519 out of libppcp and the relay supplies its own."; \
exit 1; }
@if [ -z "$(SIM_NAME)" ]; then echo "make rv6: no available iPhone simulator."; exit 1; fi
@echo "=== ppcp-relay --selftest (the instrument, before anything rests on it) ==="
@"$(PPCP_RELAY)" --selftest 2>&1 | tail -12
set -o pipefail && $(GUARD) $(BUILD_TIMEOUT_S) xcodebuild build-for-testing \
-project $(PROJECT) \
-scheme $(SCHEME) \
-configuration $(CONFIG) \
-destination '$(TEST_DEST)' \
-derivedDataPath $(DERIVED) \
-jobs $(JOBS) \
| $(XCB)
@$(MAKE) --no-print-directory _rv6_run RV6_MODE=order \
RV6_ARGS="--probe order-acceptor --connect 127.0.0.1:$(RV6_PORT)" \
RV6_WHAT="11.5c — the acceptor committed BLIND (RT-20b(ii))"
@$(MAKE) --no-print-directory _rv6_run RV6_MODE=decline \
RV6_ARGS="--probe decline --connect 127.0.0.1:$(RV6_PORT)" \
RV6_WHAT="RT-20b(iii)/(iv) — a declined comparison pairs nothing, and the window stays shut"
@$(MAKE) --no-print-directory _rv6_run RV6_MODE=pair \
RV6_ARGS="--peer initiator --connect 127.0.0.1:$(RV6_PORT)" \
RV6_WHAT="a full guided pairing against the relay's honest initiator"
# One run: hold a window open in the simulator, then point the relay at it.
_rv6_run:
@set -e; \
mkdir -p $(DERIVED); \
log=$(DERIVED)/rv6-$(RV6_MODE).log; \
rm -f "$$log"; \
echo ""; \
echo "=== $(RV6_WHAT) ==="; \
if lsof -nP -iTCP:$(RV6_PORT) -sTCP:LISTEN >/dev/null 2>&1; then \
echo "make rv6: $(RV6_PORT) is ALREADY bound — a previous run is still up."; \
echo " ⚠ Suspect the harness: a stale listener makes the relay measure"; \
echo " the wrong process, and NWListener on a taken port waits forever."; \
lsof -nP -iTCP:$(RV6_PORT) -sTCP:LISTEN; exit 1; \
fi; \
TEST_RUNNER_PPCP_RV6_PORT=$(RV6_PORT) \
TEST_RUNNER_PPCP_RV6_MODE=$(RV6_MODE) \
$(GUARD) $(TEST_TIMEOUT_S) xcodebuild test-without-building $(NO_DIAG) \
-project $(PROJECT) \
-scheme $(SCHEME) \
-configuration $(CONFIG) \
-destination '$(TEST_DEST)' \
-derivedDataPath $(DERIVED) \
-only-testing:PinPointCaptureTests/GuidedPairingRelayTests \
>"$$log" 2>&1 & \
testpid=$$!; \
trap 'kill $$testpid 2>/dev/null || true' EXIT; \
bound=""; \
i=0; \
while [ $$i -lt $$(( $(RV6_WARMUP_S) * 2 )) ]; do \
if lsof -nP -iTCP:$(RV6_PORT) -sTCP:LISTEN >/dev/null 2>&1; then bound=yes; break; fi; \
kill -0 $$testpid 2>/dev/null || break; \
sleep 0.5; i=$$(( i + 1 )); \
done; \
if [ -z "$$bound" ]; then \
echo "make rv6: nothing ever bound $(RV6_PORT)."; \
echo " ⚠ Suspect the harness before the peer: check the test SKIPPED,"; \
echo " and that the simulator reached the window at all."; \
grep -E "^RV6|✘|error:|Test run" "$$log" || tail -30 "$$log"; \
exit 1; \
fi; \
echo "port $(RV6_PORT) is bound — starting the relay"; \
relayrc=0; \
"$(PPCP_RELAY)" $(RV6_ARGS) 2>&1 || relayrc=$$?; \
testrc=0; \
wait $$testpid || testrc=$$?; \
echo "--- the acceptor's own account ---"; \
grep -E "^RV6|✔ Test|✘|↳|error:|Test run with" "$$log" || tail -40 "$$log"; \
echo " (full log: $$log)"; \
if [ $$relayrc -ne 0 ]; then echo "⛔ relay reported a FAILURE ($$relayrc)"; exit 1; fi; \
if [ $$testrc -ne 0 ]; then echo "⛔ the acceptor's own assertions FAILED"; exit 1; fi; \
echo "✔ $(RV6_WHAT)"
conform-sim: gen
@if [ ! -x "$(PPCP_SIM)" ]; then \
echo "make conform: no ppcp-sim at $(PPCP_SIM)"; \
echo " Build it: cmake --build $(LIBPPCP)/build/dev --target ppcp-sim"; \
exit 1; \
fi
@if [ -z "$(SIM_NAME)" ]; then \
echo "make conform: no available iPhone simulator found."; exit 1; \
fi
@# ⛔ **Build first, then start the counterpart.** The first version started
@# ppcp-sim and then ran `xcodebuild test`, which builds — and a compile is
@# minutes while `--run-ms` is seconds, so the simulator peer had exited before
@# the device dialled and the failure looked like a refused connection.
@# `build-for-testing` / `test-without-building` is what makes the window the
@# test's rather than the compiler's.
set -o pipefail && xcodebuild build-for-testing \
-project $(PROJECT) \
-scheme $(SCHEME) \
-configuration $(CONFIG) \
-destination '$(TEST_DEST)' \
-derivedDataPath $(DERIVED) \
-jobs $(JOBS) \
| $(XCB)
@set -e; \
portfile=$$(mktemp -t ppcp-conform-port); \
log=$$(mktemp -t ppcp-conform-log); \
"$(PPCP_SIM)" --role host --listen 0 --port-file "$$portfile" \
--declaration "$(SCENARIO_DECL)" --scenario $(SCENARIO) \
$(foreach e,$(subst $(COMMA), ,$(EXPECT)),--expect $(e)) \
--run-ms $(CONFORM_RUN_MS) >"$$log" 2>&1 & \
simpid=$$!; \
trap 'kill $$simpid 2>/dev/null || true' EXIT; \
for i in 1 2 3 4 5 6 7 8 9 10; do \
[ -s "$$portfile" ] && break; sleep 0.2; \
done; \
port=$$(cat "$$portfile" 2>/dev/null); \
if [ -z "$$port" ]; then echo "make conform: ppcp-sim never reported a port"; \
cat "$$log"; exit 1; fi; \
echo "ppcp-sim listening on $$port, scenario $(SCENARIO)"; \
testlog=$$(mktemp -t ppcp-conform-test); \
TEST_RUNNER_PPCP_CONFORM_PORT=$$port \
TEST_RUNNER_PPCP_CONFORM_ROW=$(ROW) \
TEST_RUNNER_PPCP_CONFORM_SCENARIO=$(SCENARIO) xcodebuild test-without-building $(NO_DIAG) \
-project $(PROJECT) \
-scheme $(SCHEME) \
-configuration $(CONFIG) \
-destination '$(TEST_DEST)' \
-derivedDataPath $(DERIVED) \
-only-testing:PinPointCaptureTests/ConformanceHarnessTests \
-default-test-execution-time-allowance 120 \
-maximum-test-execution-time-allowance 300 \
>"$$testlog" 2>&1 </dev/null \
|| { echo "--- ppcp-sim ---"; cat "$$log"; \
grep -E '^(◇|✔|✘|↳)|error:|Test run|\*\* TEST' "$$testlog" || true; exit 1; }; \
grep -E '^(◇|✔|✘|↳)|error:|Test run|\*\* TEST' "$$testlog" || true; \
wait $$simpid || { echo "--- ppcp-sim ---"; cat "$$log"; \
echo "make conform: ppcp-sim exited non-zero — a violation or an unmet expectation"; \
exit 1; }; \
echo "--- ppcp-sim ---"; cat "$$log"
# D9's claim — `ppcp-conform` drives this device through every applicable row of
# `PPCP-CONF` §3 and §4 and emits the matrix fragment.
#
# ⚠ **`--listen`, not `--connect`.** The device DIALS: `PpcpDirectConnector` is a
# connector and there is no plaintext listener anywhere in the app, which is also
# how `RV` 2c1's "nothing to accept on" stays easy to hold. The tool spawns one
# `ppcp-sim` per row, sequentially, on that one port — so the device dials once
# per row, which is what `ConformanceHarnessTests.ppcpConformDrivesTheDevice()`
# does.
#
# ⛔ **THE SIMULATOR STARTS FIRST, AND THEN THE TOOL.** `ppcp-conform` gives each
# row 5–8 s and moves on; a simulator takes tens of seconds to boot, install and
# launch. Starting the tool first burned every row waiting for a device that was
# still starting up — all four failed "timed out waiting for two bound channels",
# which is the same mistake as starting the counterpart before the build, one
# layer further in. So the test is launched, `CONFORM_WARMUP_S` is allowed for it
# to reach its dial loop, and only then does the tool begin binding ports.
#
# ⛔ **The verdict is the tool's exit code, not the test's.** 0 every applicable
# row passed, 1 a row failed, 2 a bad invocation, 3 NO ROW APPLIED — and 3 is not
# success: a claim naming profiles the tool has no row for has not been measured.
conform-tool: gen
@if [ ! -x "$(PPCP_CONFORM)" ]; then \
echo "make conform: no ppcp-conform at $(PPCP_CONFORM)"; \
echo " Build it: cmake --build --preset dev -j3 --target ppcp-conform ppcp-sim"; \
echo " (in $(LIBPPCP))"; \
exit 1; \
fi
@if [ -z "$(SIM_NAME)" ]; then \
echo "make conform: no available iPhone simulator found."; exit 1; \
fi
@mkdir -p $(CONFORM_OUT)
set -o pipefail && xcodebuild build-for-testing \
-project $(PROJECT) \
-scheme $(SCHEME) \
-configuration $(CONFIG) \
-destination '$(TEST_DEST)' \
-derivedDataPath $(DERIVED) \
-jobs $(JOBS) \
| $(XCB)
@set -e; \
log=$$(mktemp -t ppcp-conform-log); \
testlog=$$(mktemp -t ppcp-conform-test); \
port=$$(python3 -c 'import socket;s=socket.socket();s.bind(("127.0.0.1",0));print(s.getsockname()[1]);s.close()'); \
echo "ppcp-conform on port $$port, profiles $(CONFORM_PROFILES)"; \
echo "starting the simulator first — see the ordering note above"; \
TEST_RUNNER_PPCP_CONFORM_TOOL_PORT=$$port xcodebuild test-without-building $(NO_DIAG) \
-project $(PROJECT) \
-scheme $(SCHEME) \
-configuration $(CONFIG) \
-destination '$(TEST_DEST)' \
-derivedDataPath $(DERIVED) \
-only-testing:PinPointCaptureTests/ConformanceHarnessTests \
-default-test-execution-time-allowance 600 \
-maximum-test-execution-time-allowance 900 \
>"$$testlog" 2>&1 & \
testpid=$$!; \
trap 'kill $$testpid $$toolpid 2>/dev/null || true' EXIT; \
sleep $(CONFORM_WARMUP_S); \
"$(PPCP_CONFORM)" --profiles $(CONFORM_PROFILES) --role capture \
--listen $$port --column PinPointCapture \
--json $(CONFORM_OUT)/ppcp-conform.json \
--markdown $(CONFORM_OUT)/ppcp-conform.md \
--sim $(PPCP_SIM) --scenarios $(LIBPPCP)/tools/scenarios \
>"$$log" 2>&1 & \
toolpid=$$!; \
rc=0; wait $$toolpid || rc=$$?; \
kill $$testpid 2>/dev/null || true; \
wait $$testpid 2>/dev/null || true; \
grep -E '^(◇|✔|✘|↳)|error:|Test run|\*\* TEST' "$$testlog" || true; \
echo "--- ppcp-conform ---"; cat "$$log"; \
case $$rc in \
0) echo "make conform: every applicable row passed";; \
1) echo "make conform: A ROW FAILED — see the table above and $(CONFORM_OUT)/ppcp-conform.md";; \
2) echo "make conform: bad invocation";; \
3) echo "make conform: NO ROW APPLIED to this claim and role — not a pass";; \
*) echo "make conform: ppcp-conform exited $$rc";; \
esac; \
exit $$rc
# ─────────────────────────────────────────────────────────────────────────────
# `CONF` §5 wave 1 — the two rows where this device is a real party, in ONE
# simulator launch.
#
# make conform-iop
#
# ⛔ **Two counterparts, one launch, and that is not thrift.** Booting,
# installing and launching a simulator costs tens of seconds; a row costs twenty.
# So `ppcp-sim` is started twice — IOP-2's foreign, three-clock host on one port
# and IOP-1's reference host on another — and the suite runs once, dialling each
# in turn. Every port is bound by the tool with `--listen 0`, so nothing collides
# with another agent's run.
#
# IOP-2 three-timebase-host.json + reference-host — two machine-vision
# cameras, each on its own clock, both `convention: start` /
# `geometry: global`. Proves I19 (this device declares what it is) and
# I22 (an issued `t0` on the host's clock is CONVERTED here, not
# adopted).
# IOP-1 reference-host.json + reference-host — the full session, plus a
# `session_offer` of two stored Sessions and their replay (`MSG` §9.1,
# `ENC` 7a). `offers_rx=2` is the far end's half of the assertion.
#
# ⛔ **`--run-ms` outlives the test, deliberately.** A counterpart that exits
# first closes the link mid-row and the failure reads as a refusal.
IOP_RUN_MS ?= 200000
# Which tests the run drives. ⚠ **`make conform-iop IOP_TESTS=` runs the WHOLE
# suite**, which is how one simulator launch covers `make test-app` as well as
# the two rows: booting, installing and launching costs tens of seconds and the
# rest of the suite needs no counterpart — every test that does skips without a
# port in the environment.
IOP_TESTS ?= -only-testing:PinPointCaptureTests/ConformanceHarnessTests
conform-iop: gen
@if [ ! -x "$(PPCP_SIM)" ]; then \
echo "make conform-iop: no ppcp-sim at $(PPCP_SIM)"; exit 1; \
fi
@if [ -z "$(SIM_NAME)" ]; then \
echo "make conform-iop: no available iPhone simulator found."; exit 1; \
fi
set -o pipefail && xcodebuild build-for-testing \
-project $(PROJECT) \
-scheme $(SCHEME) \
-configuration $(CONFIG) \
-destination '$(TEST_DEST)' \
-derivedDataPath $(DERIVED) \
-jobs $(JOBS) \
| $(XCB)
@set -e; \
p2=$$(mktemp -t ppcp-iop2-port); l2=$$(mktemp -t ppcp-iop2-log); \
p1=$$(mktemp -t ppcp-iop1-port); l1=$$(mktemp -t ppcp-iop1-log); \
"$(PPCP_SIM)" --role host --listen 0 --port-file "$$p2" --log-prefix iop2 \
--declaration $(LIBPPCP)/tools/scenarios/three-timebase-host.json \
--scenario reference-host --expect violations=0 \
--run-ms $(IOP_RUN_MS) >"$$l2" 2>&1 & \
sim2=$$!; \
"$(PPCP_SIM)" --role host --listen 0 --port-file "$$p1" --log-prefix iop1 \
--declaration $(LIBPPCP)/tools/scenarios/reference-host.json \
--scenario reference-host --expect violations=0 --expect offers_rx=2 \
--run-ms $(IOP_RUN_MS) >"$$l1" 2>&1 & \
sim1=$$!; \
trap 'kill $$sim1 $$sim2 2>/dev/null || true' EXIT; \
for i in 1 2 3 4 5 6 7 8 9 10; do \
[ -s "$$p2" ] && [ -s "$$p1" ] && break; sleep 0.3; \
done; \
port2=$$(cat "$$p2" 2>/dev/null); port1=$$(cat "$$p1" 2>/dev/null); \
if [ -z "$$port2" ] || [ -z "$$port1" ]; then \
echo "make conform-iop: a ppcp-sim never reported a port"; \
cat "$$l2" "$$l1"; exit 1; fi; \
echo "IOP-2 (three-timebase-host) on $$port2, IOP-1 (reference-host) on $$port1"; \
rc=0; \
set -o pipefail; \
TEST_RUNNER_PPCP_IOP2_PORT=$$port2 TEST_RUNNER_PPCP_IOP1_PORT=$$port1 \
TEST_RUNNER_PPCP_CONFORM_ROW=iop xcodebuild test-without-building $(NO_DIAG) \
-project $(PROJECT) \
-scheme $(SCHEME) \
-configuration $(CONFIG) \
-destination '$(TEST_DEST)' \
-derivedDataPath $(DERIVED) \
$(IOP_TESTS) \
-default-test-execution-time-allowance 300 \
-maximum-test-execution-time-allowance 600 \
>$(DERIVED)/.conform-iop.log 2>&1 </dev/null || rc=$$?; \
grep -E '^(◇|✔|✘|↳)|error:|Test run|\*\* TEST' $(DERIVED)/.conform-iop.log || true; \
echo "--- ppcp-sim, IOP-2 (three-timebase-host) ---"; tail -3 "$$l2"; \
echo "--- ppcp-sim, IOP-1 (reference-host) ---"; tail -3 "$$l1"; \
kill $$sim1 $$sim2 2>/dev/null || true; \
if [ $$rc != 0 ]; then \
echo "make conform-iop: A ROW FAILED — see the transcript above"; exit $$rc; fi; \
echo "make conform-iop: both rows passed on the device side"; \
$(MAKE) --no-print-directory pull-bundles
# IOP-3 / IOP-10, the receiving half — read a bundle another implementation
# wrote, through `SessionBundleReader`.
#
# make read-bundle FILE=../PinPointStudio/docs/conformance/bundles/x.ppcpbndl
#
# ⚠ Native, no simulator: the reader is `CaptureCore` and `CaptureCore` is
# platform-free by construction (REQ-PORT-3).
read-bundle:
@if [ -z "$(FILE)" ]; then echo "make read-bundle: FILE=<path.ppcpbndl> is required"; exit 1; fi
@if [ ! -f "$(FILE)" ]; then echo "make read-bundle: no such file: $(FILE)"; exit 1; fi
@cd Packages/Core && PPCP_BUNDLE_IN="$(abspath $(FILE))" swift test -j $(JOBS) \
--filter ImportedBundleTests 2>&1 | grep -E '^(◇|✔|✘|↳)|imported |error:|Test run'
# The bundles an IOP-1 / `make interop` run wrote, copied out of the simulator's
# app container into the repository.
#
# ⚠ **`booted`, and it is the one fragile thing here.** A test writes into the
# app's Documents directory, which lives inside a container only `simctl` can
# name. With two simulators booted this picks the wrong one; the target says what
# it found rather than failing quietly.
pull-bundles:
@set -e; \
dir=$$(xcrun simctl get_app_container booted $(BUNDLE_ID) data 2>/dev/null || true); \
if [ -z "$$dir" ]; then \
echo "make pull-bundles: no booted simulator holding $(BUNDLE_ID)."; \
echo " Run a row first: make conform ROW=iop1"; exit 1; \
fi; \
mkdir -p $(BUNDLE_OUT); \
found=0; \
for b in "$$dir/Documents/interop-bundles"*/*/*.ppcpbndl; do \
[ -e "$$b" ] || continue; \
: '⚠ NOT `dir` — that is the container path this loop still needs, and'; \
: 'reusing it here cost a run: the summaries were looked for under a'; \
: 'bundle directory name and silently not found.'; \
: '⚠ The directory is `<peer>~<session>` and a `peer:` is a fresh UUID per'; \
: 'install, so the checked-in name is the SESSION half with the two bytes'; \
: 'a path cannot carry replaced. 5.1a — nothing is parsed back out of it.'; \
bdir=$$(basename $$(dirname "$$b")); \
name=$$(printf '%s' "$${bdir#*~}" | tr ':/' '--'); \
cp "$$b" "$(BUNDLE_OUT)/$$name.ppcpbndl"; \
found=$$((found+1)); \
echo " $(BUNDLE_OUT)/$$name.ppcpbndl $$(wc -c <"$$b" | tr -d ' ') bytes"; \
done; \
if [ "$$found" = "0" ]; then echo "make pull-bundles: no .ppcpbndl found"; fi; \
for j in interop-summary.json interop-acoustic-summary.json; do \
if [ -f "$$dir/Documents/$$j" ]; then \
cp "$$dir/Documents/$$j" $(CONFORM_OUT)/$$j; echo " $(CONFORM_OUT)/$$j"; \
fi; \
done
# ─────────────────────────────────────────────────────────────────────────────
# WAVE 2 — the real pair. This device, in the simulator, dialling the REAL
# PinPointStudio TLS listener.
#
# make interop HOST=127.0.0.1:9443 PSK=<64 hex chars> IDENTITY=<hex or text>
#
# ⛔ **No harness transport.** `PPCP_INTEROP_HOST` makes `ConformanceHarness`
# dial `PpcpConnector` — `Network.framework`, `RV` §5's TLS profile, the file
# with no plaintext branch in it — rather than the `DEBUG` plaintext connector
# `ppcp-sim` speaks. `PSK` is the 32-byte `K_tls` a scanned pairing code would
# have derived (`RV` §3); supplying it directly is the *shape* that produces, and
# the derivation itself is D7's own tested path.
#
# ⚠ **PinPointStudio provides the listener**, and it must be running before this
# starts: the device dials and does not retry for long. What the run does, in
# order — a hostless Session recorded and stored (one minted Shot, its Captures
# `absent`/`outside_buffer` because a simulator has no camera), then the dial,
# the `MSG` §3 handshake, the Session, a Stream per declared Source, the `arm`
# answered with a Readiness measurement, one injected swing through the real
# detector to a Candidate, and the stored Session offered and replayed.
#
# The summary is JSON: declares, candidates tx, shots rx, offers tx/accepted,
# errors. It is written inside the app container and copied to
# `$(CONFORM_OUT)/interop-summary.json`.
interop: gen
@if [ -z "$(HOST)" ] || [ -z "$(PSK)" ]; then \
echo "make interop: HOST=<host:port> and PSK=<64 hex chars> are required."; \
echo " e.g. make interop HOST=127.0.0.1:9443 PSK=$$(python3 -c 'print(\"ab\"*32)') IDENTITY=deadbeef"; \
exit 1; \
fi
@if [ -z "$(SIM_NAME)" ]; then \
echo "make interop: no available iPhone simulator found."; exit 1; \
fi
set -o pipefail && xcodebuild build-for-testing \
-project $(PROJECT) \
-scheme $(SCHEME) \
-configuration $(CONFIG) \
-destination '$(TEST_DEST)' \
-derivedDataPath $(DERIVED) \
-jobs $(JOBS) \
| $(XCB)
@set -e; \
echo "dialling $(HOST) $(HOST2) over TLS — the listeners must already be up"; \
set -o pipefail; \
TEST_RUNNER_PPCP_INTEROP_HOST=$(HOST) \
TEST_RUNNER_PPCP_INTEROP_HOST_ACOUSTIC=$(HOST2) \
TEST_RUNNER_PPCP_INTEROP_PSK=$(PSK) \
TEST_RUNNER_PPCP_INTEROP_IDENTITY=$(IDENTITY) xcodebuild test-without-building $(NO_DIAG) \
-project $(PROJECT) \
-scheme $(SCHEME) \
-configuration $(CONFIG) \
-destination '$(TEST_DEST)' \
-derivedDataPath $(DERIVED) \
-only-testing:PinPointCaptureTests/InteropTests \
-default-test-execution-time-allowance 300 \
-maximum-test-execution-time-allowance 600 \
>$(DERIVED)/.interop.log 2>&1 </dev/null || true; \
grep -E '^(◇|✔|✘|↳)|error:|Test run|\*\* TEST' $(DERIVED)/.interop.log || true
@$(MAKE) --no-print-directory pull-bundles || true
@if [ -f $(CONFORM_OUT)/interop-summary.json ]; then \
for f in $(CONFORM_OUT)/interop-summary.json $(CONFORM_OUT)/interop-acoustic-summary.json; do \
[ -f "$$f" ] || continue; echo "--- $$f ---"; cat "$$f"; \
done; \
else \
echo "make interop: no summary was written — the run did not reach the harness."; \
exit 1; \
fi
# ⛔ **The composed app against a real PinPointStudio, WITHOUT a phone.**
#
# The iOS Simulator runs as a process on this Mac and shares its network stack,
# so 127.0.0.1 inside it is PinPointStudio's own loopback. Everything that is not
# a camera is reachable in about a minute, as often as you like -- which is the
# difference between checking a protocol change and scheduling one.
#
# make interop-app HOST=127.0.0.1:<port> PSK=<64 hex> IDENTITY=<identity>
#
# ⚠ What still needs a phone: a simulator enumerates no camera, so it declares no
# camera Source. No clip, no preview pixels, and CT-S3 is unreachable because
# there is no local declaration for a foreign one to differ from. Arm, readiness,
# session_open, sync, offers, capture_request and session_resume are all here.
#
# ⚠ Unlike `interop`, this runs ONE suite and asserts almost nothing about what
# the operator did: whether Studio armed during the window is a person's choice,
# and a row that failed because nobody pressed a button is a row nobody trusts.
# It prints what crossed. Read the APP-VS-STUDIO lines.
interop-app: gen
@if [ -z "$(HOST)" ] || [ -z "$(PSK)" ]; then \
echo "make interop-app: HOST=<host:port> and PSK=<64 hex chars> are required."; \
echo " e.g. make interop-app HOST=127.0.0.1:9443 PSK=$$(python3 -c 'print(\"ab\"*32)') IDENTITY=deadbeef"; \
exit 1; \
fi
@if [ -z "$(SIM_NAME)" ]; then \
echo "make interop-app: no available iPhone simulator found."; exit 1; \
fi
set -o pipefail && xcodebuild build-for-testing \
-project $(PROJECT) \
-scheme $(SCHEME) \
-configuration $(CONFIG) \
-destination '$(TEST_DEST)' \
-derivedDataPath $(DERIVED) \
-jobs $(JOBS) \
| $(XCB)
@set -e; \
echo "dialling $(HOST) over TLS from the simulator — PinPointStudio must be listening"; \
set -o pipefail; \
TEST_RUNNER_PPCP_INTEROP_HOST=$(HOST) \
TEST_RUNNER_PPCP_INTEROP_PSK=$(PSK) \
TEST_RUNNER_PPCP_INTEROP_IDENTITY=$(IDENTITY) xcodebuild test-without-building $(NO_DIAG) \
-project $(PROJECT) \
-scheme $(SCHEME) \
-configuration $(CONFIG) \
-destination '$(TEST_DEST)' \
-derivedDataPath $(DERIVED) \
-only-testing:PinPointCaptureTests/AppAgainstStudioTests \
-default-test-execution-time-allowance 300 \
-maximum-test-execution-time-allowance 600 \
>$(DERIVED)/.interop-app.log 2>&1 </dev/null; \
grep -E '^(◇|✔|✘|↳)|APP-VS-STUDIO|error:|Test run|\*\* TEST' \
$(DERIVED)/.interop-app.log || true
# ⛔ **The hardware suite, on the connected phone, without a person in the loop.**
#
# Everything a simulator cannot do -- a real sensor, a real encoder, a real ring,
# real clip bytes -- is already covered by `DeviceSessionTests`, which injects
# synthetic swings through the app's own `observe`. What was missing was a way to
# RUN it on the device: `make deploy` installs the app, and nothing ran the tests.
#
# make test-device # the camera halves, hostless
# make test-device HOST=127.0.0.1:<port> PSK=<64 hex> IDENTITY=<id>
# # ...and against a live PinPointStudio
#
# ⚠ Every test in the suite self-skips where there is no physical camera, so this
# is safe to point at anything; it simply proves nothing on a simulator.
#
# ⚠ A device build needs provisioning, so this is `-allowProvisioningUpdates` and
# will be slower than the simulator suite the first time.
# ⚠ A device run is slower than the simulator's: provisioning, install, launch,
# and the hosted rows deliberately sleep 12 s for `session_open` and 8 s for the
# arbiter's issue hold. 600 s is the suite; a single row needs far less.
DEVICE_TIMEOUT_S ?= 900
DEVICE_ONLY_TESTING ?= PinPointCaptureTests/DeviceSessionTests
test-device: gen
@udid=$$($(MAKE) --no-print-directory _udid); \
if [ -z "$$udid" ]; then \
echo "make test-device: no connected physical device."; \
$(MAKE) --no-print-directory _udid_paired; exit 1; \
fi; \
echo "device: $$udid"; \
set -o pipefail; \
TEST_RUNNER_PPCP_INTEROP_HOST=$(HOST) \
TEST_RUNNER_PPCP_INTEROP_PSK=$(PSK) \
TEST_RUNNER_PPCP_INTEROP_IDENTITY=$(IDENTITY) \
$(GUARD) $(DEVICE_TIMEOUT_S) xcodebuild test $(NO_DIAG) \
-project $(PROJECT) \
-scheme $(SCHEME) \
-configuration $(CONFIG) \
-destination "id=$$udid" \
-derivedDataPath $(DERIVED) \
-jobs $(JOBS) \
-allowProvisioningUpdates \
-only-testing:$(DEVICE_ONLY_TESTING) \
-default-test-execution-time-allowance 300 \
-maximum-test-execution-time-allowance 600 \
>$(DERIVED)/.test-device.log 2>&1 </dev/null; \
echo $$? > $(DERIVED)/.xcodebuild-status; \
grep -E '^(◇|✔|✘|↳)|DEVICE-RUN|SKIP|error:|Test run|\*\* TEST' \
$(DERIVED)/.test-device.log || true; \
status=$$(cat $(DERIVED)/.xcodebuild-status 2>/dev/null || echo 1); \
if [ $$status -eq $(GUARD_STATUS) ]; then \
echo ""; \
echo "make test-device: TIMED OUT after $(DEVICE_TIMEOUT_S)s and was killed."; \
echo " A device suite that hangs used to hang for ever: this target was"; \
echo " the only unattended xcodebuild in this file with no \$$(GUARD)."; \
echo " Check the phone is unlocked, trusted and not showing a dialog."; \
fi; \
exit $$status
# ⛔ **Both halves of an integration run, in one command, with no person in it.**
#
# make integration STUDIO=/path/to/PinPointStudio \
# HOST=127.0.0.1:<port> PSK=<64 hex> IDENTITY=<id> \
# [CORROBORATE=1] [EXPECT_SHOTS=1]
#
# PinPointStudio runs offscreen under `tools/probes/ppcp_assert.qml`, which polls
# its own `ppcpStats()`/`shotStats()` and exits non-zero with the corroboration
# verdict in the failure line. This device drives the SHIPPING app -- AppModel,
# its delegate, its recording session -- from the simulator over real TLS.
#
# ⚠ **CORROBORATE=1 is the half a simulator cannot otherwise reach.** A simulator
# makes no sound, so a host with a working microphone hears nothing and the
# corroboration rule refuses every Shot -- the rule becomes untestable by exactly
# the automation that most needs it. `injectDetection` supplies that one input,
# which `PPCP-CONF` §2a's *injected* method already blesses.
#
# ⛔ **And it tests the RULE, not the detector.** Nothing here says either side's
# acoustic onset detection works: only that a Shot corroborated by something is
# recorded and one corroborated by nothing is not. A green run must not be read
# as more than that (PinPointStudio's own caveat, 27 Aug).
STUDIO_PROBE ?= $(HOME)/Projects/PinPointStudio/tools/probes/ppcp_assert.qml
EXPECT_SHOTS ?= 1
integration: gen
@if [ -z "$(STUDIO)" ] || [ -z "$(HOST)" ] || [ -z "$(PSK)" ]; then \
echo "make integration: STUDIO=<binary> HOST=<host:port> PSK=<64 hex> are required."; \
exit 1; \
fi
@if [ ! -f "$(STUDIO_PROBE)" ]; then \
echo "make integration: no probe at $(STUDIO_PROBE)"; exit 1; \
fi
set -o pipefail && xcodebuild build-for-testing \
-project $(PROJECT) -scheme $(SCHEME) -configuration $(CONFIG) \
-destination '$(TEST_DEST)' -derivedDataPath $(DERIVED) -jobs $(JOBS) | $(XCB)
@set -e; \
probelog=$(DERIVED)/.integration-studio.log; \
QT_QPA_PLATFORM=offscreen "$(STUDIO)" \
--probe-qml "$(STUDIO_PROBE)" --linger \
$(if $(CORROBORATE),--corroborate,) \
--expect-shots $(EXPECT_SHOTS) >"$$probelog" 2>&1 & \
studiopid=$$!; \
trap 'kill $$studiopid 2>/dev/null || true' EXIT; \
echo "PinPointStudio offscreen (pid $$studiopid), corroborate=$(if $(CORROBORATE),on,off), expect $(EXPECT_SHOTS)"; \
sleep 8; \
TEST_RUNNER_PPCP_INTEROP_HOST=$(HOST) \
TEST_RUNNER_PPCP_INTEROP_PSK=$(PSK) \
TEST_RUNNER_PPCP_INTEROP_IDENTITY=$(IDENTITY) xcodebuild test-without-building $(NO_DIAG) \
-project $(PROJECT) -scheme $(SCHEME) -configuration $(CONFIG) \
-destination '$(TEST_DEST)' -derivedDataPath $(DERIVED) \
-only-testing:PinPointCaptureTests/AppAgainstStudioTests \
-default-test-execution-time-allowance 300 \
-maximum-test-execution-time-allowance 600 \
>$(DERIVED)/.integration-device.log 2>&1 </dev/null || true; \
grep -E '^(◇|✔|✘|↳)|APP-VS-STUDIO|error:|Test run|\*\* TEST' \
$(DERIVED)/.integration-device.log || true; \
echo "--- PinPointStudio ---"; \
grep -E "PROBE (START|RESULT|STATS)" "$$probelog" || tail -5 "$$probelog"; \
: '⛔ THE VERDICT IS THE LOG LINE, NOT THE EXIT CODE. With --linger a host'; \
: 'that passed is still up (the device half dials it again after the clip'; \
: 'lands); it is ended here and read from what it printed. A host that'; \
: 'failed or timed out exited on its own and its code is what it was.'; \
if kill -0 $$studiopid 2>/dev/null; then \
kill $$studiopid 2>/dev/null || true; wait $$studiopid 2>/dev/null || true; \
if grep -q "PROBE RESULT PASS" "$$probelog"; then rc=0; else rc=1; fi; \
else \
wait $$studiopid; rc=$$?; \
fi; \
if [ $$rc -ne 0 ]; then \
echo "make integration: the HOST half failed — see its PROBE RESULT line"; exit $$rc; \
fi
# ⛔ **BOTH HALVES, ON REAL HARDWARE, WITH NO PERSON IN THE LOOP.**
#
# make integration-device STUDIO=/path/to/PinPointStudio [EXPECT_CLIPS=1]
#
# `make integration` already owns both processes and reads both verdicts
# machine-readably -- but it runs the SIMULATOR, which has no camera and can
# therefore never produce a clip, which is the one thing the swing-video leg
# needs proving. This is that target pointed at the phone.
#
# The host half runs offscreen under `ppcp_assert.qml`, which performs the
# procedure a person performs -- enable the phone's camera, WAIT FOR CLOCK
# AGREEMENT UNDER 5 ms, start the capture session -- and then asserts on the clip
# chain rather than on shots. The device half drives the shipping app through
# `DeviceSessionTests`, arming itself and injecting its own swing.
#
# ⛔ **NEVER `devicectl --console` HERE.** It bridges the phone's stdout and holds
# a CoreDevice tunnel: the kernel re-enumerates the device, every usbmux tunnel
# dies, and the link under measurement is killed by the act of watching it. The
# phone's diagnostics come off afterwards with `copy from`, which touches
# nothing (see pull-diags).
#
# ⚠ `/usr/bin/log`, never bare `log` -- the latter is a zsh builtin that silently
# returns nothing.
INTEGRATION_OUT ?= $(HOME)/pinpoint-diags/$(shell date +%Y-%m-%d-%H%M%S)-integration
EXPECT_CLIPS ?= 1