@@ -103,7 +103,8 @@ $$ LANGUAGE plpgsql;`
103103 }
104104 }
105105
106- // Verify PERFORM statements are injected before the executable lines
106+ // Verify PERFORM statements are injected after the executable lines (coverage-after-execute),
107+ // except for terminal statements (RETURN) which keep PERFORM before.
107108 // Note: After instrumentation, line numbers shift due to inserted PERFORM statements
108109 // We just verify that PERFORM statements exist for each coverage point
109110 for _ , cp := range instrumented .Locations {
@@ -236,3 +237,133 @@ $$ LANGUAGE plpgsql;`
236237 }
237238 t .Logf ("Coverage points: %d (may be 0 for malformed SQL)" , len (instrumented .Locations ))
238239}
240+
241+ func TestIsTerminalSegment (t * testing.T ) {
242+ tests := []struct {
243+ name string
244+ segment string
245+ terminal bool
246+ }{
247+ {"RETURN value" , "RETURN a + b" , true },
248+ {"RETURN bare" , "RETURN" , true },
249+ {"RAISE EXCEPTION" , "RAISE EXCEPTION 'error'" , true },
250+ {"RAISE with string (default EXCEPTION)" , "RAISE 'something went wrong'" , true },
251+ {"RAISE bare re-raise" , "RAISE" , true },
252+ {"RAISE NOTICE" , "RAISE NOTICE 'hello'" , false },
253+ {"RAISE WARNING" , "RAISE WARNING 'warn'" , false },
254+ {"RAISE INFO" , "RAISE INFO 'info'" , false },
255+ {"RAISE LOG" , "RAISE LOG 'log'" , false },
256+ {"RAISE DEBUG" , "RAISE DEBUG 'debug'" , false },
257+ {"assignment" , "discount_rate := 0.20" , false },
258+ {"IF block" , "IF x > 0 THEN\n y := 1" , false },
259+ {"PERFORM" , "PERFORM some_function()" , false },
260+ {"SELECT" , "SELECT 1 INTO result" , false },
261+ {"comment then RETURN" , "-- comment\n RETURN 42" , true },
262+ {"comment then assignment" , "-- comment\n x := 1" , false },
263+ {"empty" , "" , false },
264+ }
265+
266+ for _ , tt := range tests {
267+ t .Run (tt .name , func (t * testing.T ) {
268+ got := isTerminalSegment (tt .segment )
269+ if got != tt .terminal {
270+ t .Errorf ("isTerminalSegment(%q) = %v, want %v" , tt .segment , got , tt .terminal )
271+ }
272+ })
273+ }
274+ }
275+
276+ func TestInstrumentBody_CoverageAfterExecute (t * testing.T ) {
277+ // Verify that for non-terminal statements, NOTIFY comes after the statement,
278+ // and for RETURN, NOTIFY comes before.
279+ sql := `CREATE OR REPLACE FUNCTION example(x INT)
280+ RETURNS INT AS $$
281+ BEGIN
282+ x := x + 1;
283+ RETURN x;
284+ END;
285+ $$ LANGUAGE plpgsql;`
286+
287+ stmts := parser .ParseStatements (sql )
288+ if len (stmts ) == 0 {
289+ t .Fatal ("ParseStatements() returned no statements" )
290+ }
291+
292+ instrumentedSQL , coveragePoints := instrumentBody (stmts [0 ], "test.sql" , true , "PERFORM" )
293+ if len (coveragePoints ) != 2 {
294+ t .Fatalf ("expected 2 coverage points, got %d" , len (coveragePoints ))
295+ }
296+
297+ // The assignment "x := x + 1" should have NOTIFY *after* it.
298+ assignSignal := coveragePoints [0 ].SignalID
299+ assignNotify := fmt .Sprintf ("PERFORM pg_notify('pgcov', '%s');" , assignSignal )
300+ assignIdx := strings .Index (instrumentedSQL , assignNotify )
301+ assignStmtIdx := strings .Index (instrumentedSQL , "x := x + 1" )
302+ if assignIdx < 0 || assignStmtIdx < 0 {
303+ t .Fatal ("could not find assignment or its notify in instrumented SQL" )
304+ }
305+ if assignIdx <= assignStmtIdx {
306+ t .Errorf ("assignment: NOTIFY at %d should come after statement at %d (coverage-after-execute)" , assignIdx , assignStmtIdx )
307+ }
308+
309+ // The RETURN should have NOTIFY *before* it (terminal statement).
310+ returnSignal := coveragePoints [1 ].SignalID
311+ returnNotify := fmt .Sprintf ("PERFORM pg_notify('pgcov', '%s');" , returnSignal )
312+ returnIdx := strings .Index (instrumentedSQL , returnNotify )
313+ returnStmtIdx := strings .Index (instrumentedSQL , "RETURN x" )
314+ if returnIdx < 0 || returnStmtIdx < 0 {
315+ t .Fatal ("could not find RETURN or its notify in instrumented SQL" )
316+ }
317+ if returnIdx >= returnStmtIdx {
318+ t .Errorf ("RETURN: NOTIFY at %d should come before statement at %d (terminal statement)" , returnIdx , returnStmtIdx )
319+ }
320+
321+ t .Log (instrumentedSQL )
322+ }
323+
324+ func TestInstrumentBody_RaiseExceptionBeforeNotify (t * testing.T ) {
325+ // Use standalone RAISE statements so each is its own segment.
326+ sql := `CREATE OR REPLACE FUNCTION check_positive(x INT)
327+ RETURNS VOID AS $$
328+ BEGIN
329+ RAISE NOTICE 'checking value: %', x;
330+ RAISE EXCEPTION 'negative value: %', x;
331+ END;
332+ $$ LANGUAGE plpgsql;`
333+
334+ stmts := parser .ParseStatements (sql )
335+ if len (stmts ) == 0 {
336+ t .Fatal ("ParseStatements() returned no statements" )
337+ }
338+
339+ instrumentedSQL , coveragePoints := instrumentBody (stmts [0 ], "test.sql" , true , "PERFORM" )
340+ if len (coveragePoints ) != 2 {
341+ t .Fatalf ("expected 2 coverage points, got %d" , len (coveragePoints ))
342+ }
343+
344+ // RAISE NOTICE is non-terminal — NOTIFY should come after it.
345+ raiseNoticeSignal := coveragePoints [0 ].SignalID
346+ raiseNoticeNotify := fmt .Sprintf ("PERFORM pg_notify('pgcov', '%s');" , raiseNoticeSignal )
347+ raiseNoticeIdx := strings .Index (instrumentedSQL , raiseNoticeNotify )
348+ raiseNoticeStmtIdx := strings .Index (instrumentedSQL , "RAISE NOTICE" )
349+ if raiseNoticeIdx < 0 || raiseNoticeStmtIdx < 0 {
350+ t .Fatal ("could not find RAISE NOTICE or its notify" )
351+ }
352+ if raiseNoticeIdx <= raiseNoticeStmtIdx {
353+ t .Errorf ("RAISE NOTICE: NOTIFY at %d should come after statement at %d" , raiseNoticeIdx , raiseNoticeStmtIdx )
354+ }
355+
356+ // RAISE EXCEPTION is terminal — NOTIFY should come before it.
357+ raiseExcSignal := coveragePoints [1 ].SignalID
358+ raiseExcNotify := fmt .Sprintf ("PERFORM pg_notify('pgcov', '%s');" , raiseExcSignal )
359+ raiseExcIdx := strings .Index (instrumentedSQL , raiseExcNotify )
360+ raiseExcStmtIdx := strings .Index (instrumentedSQL , "RAISE EXCEPTION" )
361+ if raiseExcIdx < 0 || raiseExcStmtIdx < 0 {
362+ t .Fatal ("could not find RAISE EXCEPTION or its notify" )
363+ }
364+ if raiseExcIdx >= raiseExcStmtIdx {
365+ t .Errorf ("RAISE EXCEPTION: NOTIFY at %d should come before statement at %d" , raiseExcIdx , raiseExcStmtIdx )
366+ }
367+
368+ t .Log (instrumentedSQL )
369+ }
0 commit comments