|
1
|
/*------------------------------------------------------------------------------
|
|
2
|
tc11180x6_deadframe.p
|
|
3
|
|
|
4
|
Regression probe X6 for review finding "oldFocusGone cannot tell 'the frame
|
|
5
|
died during THIS dispatch' from 'the frame was ALREADY dead'"
|
|
6
|
(ThinClient.processProgressEvent, #11180b post-2026-08-12).
|
|
7
|
|
|
8
|
boolean oldFocusGone = oldFrame != null && !oldFrame.isAlive(); // <-- post-state only
|
|
9
|
|
|
10
|
Frame.isAlive() is a sticky flag that nothing ever resets, and the value is
|
|
11
|
sampled AFTER the LEAVE dispatch, so the predicate is also true for a frame
|
|
12
|
that was destroyed long BEFORE this APPLY arrived. A dead frame keeps the
|
|
13
|
focus: destroyFrame() marks a regular visible frame setAlive(false), runs
|
|
14
|
selectiveDisable() and DEFERS the hide, and the deadFrames state sync flips
|
|
15
|
liveness with no visibility, enablement or focus change at all.
|
|
16
|
|
|
17
|
Consequence claimed by the review: an ORDINARY APPLY 'ENTRY' issued while the
|
|
18
|
focus still sits in an already-dead frame takes the frame-teardown
|
|
19
|
"WITHOUT-steal" arm (src.requestFocus(); evt.consume(); return;) and therefore
|
|
20
|
skips BOTH sendEntry(frame) (the target FRAME's ENTRY trigger) AND
|
|
21
|
invokeTriggers(src, .., SE_ENTRY) (the target widget's ON ENTRY). Trunk fired
|
|
22
|
both. Nothing in this probe involves a LEAVE trigger, a deletion mid-LEAVE or
|
|
23
|
a focus steal - it is plain, everyday 4GL.
|
|
24
|
|
|
25
|
This is the ONLY probe in the suite where the frame dies OUTSIDE a LEAVE
|
|
26
|
trigger; every other #11180 probe (tc11180d/e/f*, tc11180x1..x5,
|
|
27
|
test-11180-f/-g) deletes from inside the LEAVE, i.e. the legitimate case.
|
|
28
|
|
|
29
|
Two independent phases, two independent target frames, so a phase that fails
|
|
30
|
to reach the branch on FWD does not spoil the other:
|
|
31
|
|
|
32
|
Phase A - STATIC frame scoped to an internal procedure. Returning from the
|
|
33
|
procedure ends the frame's scope -> markFrames(MarkEntry.DESTROY)
|
|
34
|
-> destroyFrame(). No LEAVE trigger runs.
|
|
35
|
Phase B - DYNAMIC frame holding a read-only BROWSE, deleted at MAINLINE
|
|
36
|
(not from any trigger). The browse is used because a read-only
|
|
37
|
browse reliably remains the focus widget after its frame is
|
|
38
|
destroyed (established by tc11180d_false_steal.p).
|
|
39
|
|
|
40
|
Each phase then issues a plain APPLY 'ENTRY' at a fill-in in a different,
|
|
41
|
fully alive frame and records whether that frame's and that widget's ENTRY
|
|
42
|
triggers fired.
|
|
43
|
|
|
44
|
READ (per phase):
|
|
45
|
* target FRAME ENTRY + target widget ENTRY both present
|
|
46
|
-> correct (native OE, and FWD once oldFocusGone is a transition test)
|
|
47
|
* either ENTRY absent, FOCUS reported on the dead frame's widget
|
|
48
|
-> the reviewed defect reproduces
|
|
49
|
|
|
50
|
MEASURED 2026-08-14 - NEITHER PHASE REACHES THE BRANCH. Both phases MATCH
|
|
51
|
native OE (docs/11180b/4gl/tc11180x6.log vs deploy/client/tc11180x6.log, FWD at
|
|
52
|
revno 16725), because neither idiom actually produces the required state, namely
|
|
53
|
"focus sitting in a frame that is already dead":
|
|
54
|
|
|
55
|
Phase A - FWD leaves fDoomed ALIVE. FOCUS is still FILL-IN/fDoom after the
|
|
56
|
procedure returns AND both target ENTRYs fire; a dead frame would
|
|
57
|
have made the pre-fix oldFocusGone true and dropped both. So an
|
|
58
|
internal-procedure-scoped frame whose widget holds focus is not
|
|
59
|
marked dead here.
|
|
60
|
Phase B - FOCUS goes <none/invalid> in BOTH engines after the mainline
|
|
61
|
DELETE WIDGET, so inFocus/oldFrame are null and oldFocusGone is
|
|
62
|
false either way. The tc11180d idiom "a read-only browse survives
|
|
63
|
as the focus widget after its frame dies" holds only INSIDE a LEAVE
|
|
64
|
dispatch, not at mainline.
|
|
65
|
|
|
66
|
So the r4 finding "oldFocusGone is a post-state test" remains UNREACHED rather
|
|
67
|
than refuted, and its proposed fix is a provable no-op on this probe (which is
|
|
68
|
itself the useful result: no regression). A probe that settles it must create a
|
|
69
|
dead frame while a LIVE widget still holds focus; these two routes do not.
|
|
70
|
See 11180b/11180b_v4_proposals.txt for the backed-out fix.
|
|
71
|
|
|
72
|
RESIDUAL DIVERGENCE this probe DID find, unrelated to the above (see phase B
|
|
73
|
setup, the APPLY that parks focus in frame B): OE fires fT1 LEAVE then
|
|
74
|
frmT1 LEAVE; FWD omits the FRAME LEAVE. Structurally upstream of the reviewed
|
|
75
|
code - sendLeave(oldFrame, frame) runs before oldFocusGone is computed.
|
|
76
|
|
|
77
|
Expected (native OpenEdge, MEASURED): both phases fire both ENTRY triggers.
|
|
78
|
Expected (FWD 16725, MEASURED): identical in both measured windows; the only
|
|
79
|
difference anywhere in the log is the missing frmT1 LEAVE noted above.
|
|
80
|
|
|
81
|
Runs head-less on both systems - the whole path is driven synchronously from
|
|
82
|
APPLY 'ENTRY'. Log: tc11180x6.log (client cwd).
|
|
83
|
------------------------------------------------------------------------------*/
|
|
84
|
|
|
85
|
DEFINE TEMP-TABLE tt NO-UNDO FIELD nm AS CHARACTER.
|
|
86
|
DEFINE QUERY qB FOR tt SCROLLING.
|
|
87
|
|
|
88
|
DEFINE VARIABLE fDoom AS CHARACTER NO-UNDO FORMAT "x(8)" INITIAL "doom".
|
|
89
|
DEFINE VARIABLE fT1 AS CHARACTER NO-UNDO FORMAT "x(8)" INITIAL "tgt1".
|
|
90
|
DEFINE VARIABLE fT2 AS CHARACTER NO-UNDO FORMAT "x(8)" INITIAL "tgt2".
|
|
91
|
DEFINE VARIABLE seq AS INTEGER NO-UNDO.
|
|
92
|
|
|
93
|
DEFINE VARIABLE hFrmB AS HANDLE NO-UNDO.
|
|
94
|
DEFINE VARIABLE hBrwB AS HANDLE NO-UNDO.
|
|
95
|
|
|
96
|
/* the two measured destinations - both alive and untouched throughout */
|
|
97
|
DEFINE FRAME frmT1 fT1
|
|
98
|
WITH TITLE "FRAME-T1 (phase A target)" AT ROW 10 COLUMN 2 SIZE 44 BY 3 SIDE-LABELS.
|
|
99
|
|
|
100
|
DEFINE FRAME frmT2 fT2
|
|
101
|
WITH TITLE "FRAME-T2 (phase B target)" AT ROW 14 COLUMN 2 SIZE 44 BY 3 SIDE-LABELS.
|
|
102
|
|
|
103
|
/* Suppress the "Press SPACE BAR to continue" pause on frame hide. */
|
|
104
|
PAUSE 0 BEFORE-HIDE.
|
|
105
|
|
|
106
|
PROCEDURE logit:
|
|
107
|
DEFINE INPUT PARAMETER m AS CHARACTER NO-UNDO.
|
|
108
|
seq = seq + 1.
|
|
109
|
OUTPUT TO "tc11180x6.log" APPEND.
|
|
110
|
PUT UNFORMATTED STRING(seq, "99") " " m SKIP.
|
|
111
|
OUTPUT CLOSE.
|
|
112
|
END PROCEDURE.
|
|
113
|
|
|
114
|
/* NB FOCUS:NAME is the UNKNOWN value for a dynamic widget that was never given a
|
|
115
|
NAME, and "text" + ? is ? in 4GL - which silently collapsed the whole line to a
|
|
116
|
bare "?" in the 2026-08-14 runs. Guard the concatenation. */
|
|
117
|
PROCEDURE logFocus:
|
|
118
|
DEFINE INPUT PARAMETER m AS CHARACTER NO-UNDO.
|
|
119
|
RUN logit(m + (IF VALID-HANDLE(FOCUS)
|
|
120
|
THEN STRING(FOCUS:TYPE)
|
|
121
|
+ "/"
|
|
122
|
+ (IF FOCUS:NAME = ? THEN "<noname>" ELSE FOCUS:NAME)
|
|
123
|
ELSE "<none/invalid>")).
|
|
124
|
END PROCEDURE.
|
|
125
|
|
|
126
|
/* ---- phase A: park focus in a frame that dies at SCOPE END --------------- */
|
|
127
|
/* FRAME fDoomed is referenced ONLY inside this internal procedure, so it is
|
|
128
|
scoped to the procedure and is destroyed when the procedure returns. No
|
|
129
|
LEAVE trigger is involved - this is scope-end destruction, the path the
|
|
130
|
review names (markFrames(MarkEntry.DESTROY) -> destroyFrame). */
|
|
131
|
PROCEDURE parkInDoomedFrame:
|
|
132
|
DISPLAY fDoom WITH FRAME fDoomed
|
|
133
|
TITLE "FRAME-DOOMED (dies at scope end)" AT ROW 2 COLUMN 2 SIZE 44 BY 3 SIDE-LABELS.
|
|
134
|
ENABLE fDoom WITH FRAME fDoomed.
|
|
135
|
APPLY "ENTRY" TO fDoom IN FRAME fDoomed.
|
|
136
|
RUN logFocus(" phase A: focus parked, procedure about to return; FOCUS = ").
|
|
137
|
END PROCEDURE.
|
|
138
|
|
|
139
|
/* ---- target triggers (the measured emits) ------------------------------- */
|
|
140
|
ON ENTRY OF FRAME frmT1
|
|
141
|
RUN logit("frmT1 ENTRY <== phase A target FRAME ENTRY (MUST fire)").
|
|
142
|
ON LEAVE OF FRAME frmT1
|
|
143
|
RUN logit("frmT1 LEAVE").
|
|
144
|
ON ENTRY OF fT1 IN FRAME frmT1
|
|
145
|
RUN logit("fT1 ENTRY <== phase A target widget ENTRY (MUST fire; dropped by bug)").
|
|
146
|
ON LEAVE OF fT1 IN FRAME frmT1
|
|
147
|
RUN logit("fT1 LEAVE").
|
|
148
|
|
|
149
|
ON ENTRY OF FRAME frmT2
|
|
150
|
RUN logit("frmT2 ENTRY <== phase B target FRAME ENTRY (MUST fire)").
|
|
151
|
ON LEAVE OF FRAME frmT2
|
|
152
|
RUN logit("frmT2 LEAVE").
|
|
153
|
ON ENTRY OF fT2 IN FRAME frmT2
|
|
154
|
RUN logit("fT2 ENTRY <== phase B target widget ENTRY (MUST fire; dropped by bug)").
|
|
155
|
ON LEAVE OF fT2 IN FRAME frmT2
|
|
156
|
RUN logit("fT2 LEAVE").
|
|
157
|
|
|
158
|
/* ---- main --------------------------------------------------------------- */
|
|
159
|
OUTPUT TO "tc11180x6.log".
|
|
160
|
PUT UNFORMATTED "=== tc11180x6_deadframe start ===" SKIP.
|
|
161
|
OUTPUT CLOSE.
|
|
162
|
|
|
163
|
CREATE tt. tt.nm = "row1".
|
|
164
|
CREATE tt. tt.nm = "row2".
|
|
165
|
CREATE tt. tt.nm = "row3".
|
|
166
|
|
|
167
|
ENABLE fT1 WITH FRAME frmT1.
|
|
168
|
VIEW FRAME frmT1.
|
|
169
|
ENABLE fT2 WITH FRAME frmT2.
|
|
170
|
VIEW FRAME frmT2.
|
|
171
|
|
|
172
|
/* ===================== PHASE A - scope-end destruction ==================== */
|
|
173
|
RUN logit("=== PHASE A: static frame destroyed at SCOPE END, then a plain APPLY ===").
|
|
174
|
RUN parkInDoomedFrame.
|
|
175
|
RUN logit(" phase A: parkInDoomedFrame returned - FRAME fDoomed scope has ENDED").
|
|
176
|
RUN logFocus(" phase A: FOCUS after scope end = ").
|
|
177
|
|
|
178
|
RUN logit("--- phase A MEASURED WINDOW: APPLY ENTRY to fT1 (ordinary APPLY) ---").
|
|
179
|
APPLY "ENTRY" TO fT1 IN FRAME frmT1.
|
|
180
|
RUN logFocus("--- phase A done; FOCUS = ").
|
|
181
|
RUN logit("--- phase A EXPECT (OE/fixed): frmT1 ENTRY + fT1 ENTRY, FOCUS=FILL-IN/fT1 ---").
|
|
182
|
|
|
183
|
/* ===================== PHASE B - mainline DELETE WIDGET =================== */
|
|
184
|
RUN logit("=== PHASE B: dynamic frame + browse deleted at MAINLINE, then a plain APPLY ===").
|
|
185
|
|
|
186
|
CREATE FRAME hFrmB
|
|
187
|
ASSIGN TITLE = "FRAME-B (browse source, deleted at mainline)"
|
|
188
|
ROW = 6 COLUMN = 2 WIDTH = 34 HEIGHT = 5
|
|
189
|
BOX = TRUE VISIBLE = FALSE.
|
|
190
|
|
|
191
|
OPEN QUERY qB FOR EACH tt.
|
|
192
|
|
|
193
|
CREATE BROWSE hBrwB
|
|
194
|
ASSIGN FRAME = hFrmB
|
|
195
|
QUERY = QUERY qB:HANDLE
|
|
196
|
X = 4 Y = 4 WIDTH = 30 DOWN = 2
|
|
197
|
SEPARATORS = TRUE ROW-MARKERS = FALSE
|
|
198
|
SENSITIVE = TRUE VISIBLE = TRUE.
|
|
199
|
hBrwB:ADD-COLUMNS-FROM(BUFFER tt:HANDLE).
|
|
200
|
hBrwB:READ-ONLY = TRUE.
|
|
201
|
|
|
202
|
hFrmB:VISIBLE = TRUE.
|
|
203
|
|
|
204
|
RUN logit(" phase B: APPLY ENTRY to the browse (park focus in frame B)").
|
|
205
|
APPLY "ENTRY" TO hBrwB.
|
|
206
|
RUN logFocus(" phase B: FOCUS parked = ").
|
|
207
|
|
|
208
|
/* Delete the FRAME only, from MAINLINE - no trigger, no LEAVE, no steal.
|
|
209
|
The browse deliberately survives so it remains the focus widget. */
|
|
210
|
RUN logit(" phase B: DELETE WIDGET frame B at mainline (no trigger involved)").
|
|
211
|
IF VALID-HANDLE(hFrmB) THEN DELETE WIDGET hFrmB.
|
|
212
|
RUN logFocus(" phase B: FOCUS after the frame was deleted = ").
|
|
213
|
|
|
214
|
RUN logit("--- phase B MEASURED WINDOW: APPLY ENTRY to fT2 (ordinary APPLY) ---").
|
|
215
|
APPLY "ENTRY" TO fT2 IN FRAME frmT2.
|
|
216
|
RUN logFocus("--- phase B done; FOCUS = ").
|
|
217
|
RUN logit("--- phase B EXPECT (OE/fixed): frmT2 ENTRY + fT2 ENTRY, FOCUS=FILL-IN/fT2 ---").
|
|
218
|
|
|
219
|
RUN logit("=== tc11180x6_deadframe end ===").
|
|
220
|
QUIT.
|