theory / pgtap

PostgreSQL Unit Testing Suite
https://pgtap.org
984 stars 92 forks source link

Feature: has_trigger_events() #337

Open AWolf81 opened 5 months ago

AWolf81 commented 5 months ago

I'm pretty new to pgTAP but I think there is no easy way to check if a trigger event is avaliable at a given table.

rules_are is similar but that's not working for triggers.

The line in question of a schema that the function will test is marked in the below example definition:

CREATE TRIGGER test_trigger AFTER INSERT OR UPDATE ON test_table FOR EACH ROW EXECUTE FUNCTION test_trigger_function();

So the idea is to add a has_trigger_events function.

I think the following code is exactly implementing that function:

-- has_trigger_events_test.sql
BEGIN;
SELECT plan(13);
-- SELECT * FROM no_plan();

-- Define a test table
CREATE TABLE test_table (
    id SERIAL PRIMARY KEY,
    name TEXT
);

-- Create a trigger on the test table
CREATE OR REPLACE FUNCTION test_trigger_function()
RETURNS TRIGGER AS $$
BEGIN
    -- Trigger function logic here
    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER test_trigger
AFTER INSERT OR UPDATE ON test_table
FOR EACH ROW
EXECUTE FUNCTION test_trigger_function();

-- has_trigger_events test function
-- Parameters:
--   p_table_name            - name of table
--   p_trigger_name          - name of trigger function
--   p_expected_events_array - array of events 
--   p_test_description
-- Returns:
--   test_result text
CREATE OR REPLACE FUNCTION has_trigger_events(
    p_table_name text,
    p_trigger_name text,
    p_expected_events_array text[],
    p_test_description text
)
RETURNS TEXT AS $$
DECLARE
    found_events TEXT[];
    extra_events TEXT[];
    missing_events TEXT[];
    result TEXT;
BEGIN
    -- Check if the trigger exists
    IF NOT EXISTS (
        SELECT 1
        FROM information_schema.triggers
        WHERE event_object_table = p_table_name
        AND trigger_name = p_trigger_name
    ) THEN
        RETURN ok(false, 'Trigger ' || p_trigger_name || ' does not exist on table ' || p_table_name);
    END IF;

    -- Fetch trigger events based on provided parameters
    found_events := _get_trigger_events(p_table_name, p_trigger_name);

    -- RAISE NOTICE 'test event: %', ARRAY_TO_STRING(found_events, ', ');

    -- Compare expected events with found events to identify missing and extra events
    missing_events := ARRAY(
        SELECT unnest(p_expected_events_array)
        EXCEPT
        SELECT unnest(found_events)
    );

    extra_events := ARRAY(
        SELECT unnest(found_events)
        EXCEPT
        SELECT unnest(p_expected_events_array)
    );

    -- Generate error message using _are function
    result := _are(
        'events',
        extra_events,
        missing_events,
        p_test_description
    );

    RETURN result;
END;
$$ LANGUAGE plpgsql;

-- Reusable function to fetch trigger events based on provided parameters
-- Parameters:
--   p_table_name - name of table
--   p_triggger_name - name of trigger function
--   p_expected_timing - optional timing e.g. 'AFTER'
CREATE OR REPLACE FUNCTION _get_trigger_events(
    p_table_name text,
    p_trigger_name text,
    p_expected_timing text DEFAULT NULL
)
RETURNS TEXT[] AS $$
DECLARE
    trigger_timing_filter text;
BEGIN
    IF p_expected_timing IS NOT NULL THEN
        trigger_timing_filter := ' AND trigger_timing = ' || quote_literal(p_expected_timing);
    ELSE
        trigger_timing_filter := '';
    END IF;

    RETURN ARRAY(
        SELECT upper(event_manipulation)
        FROM information_schema.triggers
        WHERE event_object_table = p_table_name
        AND trigger_name = p_trigger_name
        || trigger_timing_filter
    );
END;
$$ LANGUAGE plpgsql;

-- Test Case 1: No missing or extra events (Both INSERT and UPDATE are expected).
SELECT * FROM check_test(
    has_trigger_events('test_table', 'test_trigger', ARRAY['INSERT', 'UPDATE'], 'should have no missing or extra events'),
    true,
    'has_trigger_event( tab, trigger, events, description )'
);

-- Test Case 2: Missing event (Only INSERT is expected).
-- counts as 3 tests has_trigger + description correct + diag correct
SELECT * FROM check_test(
    has_trigger_events('test_table', 'test_trigger', ARRAY['INSERT'], 'should fail with extra event UPDATE'),
    false,
    'has_trigger_events( tab, trigger, events, description )',
    'should fail with extra event UPDATE',
    E'    Extra events:\n        "UPDATE"'
);

-- Test Case 3: Extra event (Only UPDATE is expected).
SELECT * FROM check_test(
    has_trigger_events('test_table', 'test_trigger', ARRAY['UPDATE'], 'should fail with extra event INSERT'),
    false,
    'has_trigger_events( tab, trigger, events, description )',
    'should fail with extra event INSERT',
    E'    Extra events:\n        "INSERT"'
);

-- Test Case 4: Both missing and extra events (Only DELETE is expected).
SELECT * FROM check_test(
    has_trigger_events('test_table', 'test_trigger', ARRAY['DELETE'], 'should fail with both missing and extra events'), 
    false,
    'has_trigger_events( tab, trigger, events, description )',
    'should fail with both missing and extra events',
    E'    Extra events:\n        "INSERT"\n        "UPDATE"\n    Missing events:\n        "DELETE"'
);

-- Test Case 5: Trigger does not exist.
SELECT * FROM check_test(
    has_trigger_events('test_table', 'non_existent_trigger', ARRAY['INSERT'], 'should fail - trigger does not exist'),
    false,
    'has_trigger_events( tab, non_existent_trigger, events, description )'
);

-- Test Case 6: Table does not exist.
SELECT * FROM check_test(
    has_trigger_events('non_existent_table', 'test_trigger', ARRAY['INSERT'], 'should fail - table does not exist'),
    false,
    'has_trigger_events( non_existent_table, trigger, events, description )'
);

-- Test Case 7: Empty events.
SELECT * FROM check_test(
    has_trigger_events('test_table', 'test_trigger', ARRAY[]::TEXT[], 'should fail with both missing and extra events'),
    false,
    'has_trigger_events( table, trigger, ARRAY[], description )'
);

SELECT * FROM finish();
ROLLBACK;

-- End of test case

I haven't checked how to add it to pgTAP but if there is a guide on how to contribute, I could create a PR if this feature is interesting for pgTAP.

If this function is not interesting, that's also no problem.

The tests above are covering all cases.

Topics not covered in the function

The mentioned missing parts would be nice to have but I think they're all optional.

theory commented 5 months ago

This seems like it would be a useful function to complement triggers_are(), has_trigger(), and trigger_is(). Name it something like trigger_events_are(), which I think fits better with pgTAP's naming conventions (e.g., all the _privs_are() functions). Would also need a variant that takes the schema name. I suspect the signatures would be:

SELECT trigger_events_are ( :schema, :table, :events, :description );
SELECT trigger_events_are ( :schema, :table, :events );
SELECT trigger_events_are ( :table, :events, :description );
SELECT trigger_events_are ( :table, :events );

To add those functions to the project, append them to sql/pgtap.sql.in and to sql/pgtap--1.3.3--1.3.4.sql and put the tests in test/sql/trigger.sql.

AWolf81 commented 5 months ago

@theory thanks for the advice how to add this.

Yes, you're right, naming trigger_events_are is better.

I'll create a PR once I have this ready.

At the moment, I'm not sure why my tests are failing. I have to check this. Or do you have an idea what I'm doing wrong? Maybe I have to run the tests with a differnt command?

If I'm running make installcheck I'm getting the following output:

     ... other tests are passing
     ruletap                      ... ok        17680 ms
     resultset                    ... ok         8918 ms
     util                         ... ok         2125 ms
     policy                       ... ok         4164 ms
     trigger                      ... FAILED     3666 ms
     istap                        ... ok         2536 ms

=======================
 1 of 39 tests failed. 
=======================

The differences that caused some tests to fail can be viewed in the
file "/home/alexander/awolf81-fork-pgtap/regression.diffs".  A copy of the test summary that you see
above is saved in the file "/home/alexander/awolf81-fork-pgtap/regression.out".

make: *** [/usr/lib/postgresql/12/lib/pgxs/src/makefiles/pgxs.mk:420: installcheck] Error 1

And the regression.diffs looks like this:

diff -U3 /home/alexander/awolf81-fork-pgtap/test/expected/trigger.out /home/alexander/awolf81-fork-pgtap/results/trigger.out
--- /home/alexander/awolf81-fork-pgtap/test/expected/trigger.out    2024-05-12 21:58:18.365706798 +0200
+++ /home/alexander/awolf81-fork-pgtap/results/trigger.out  2024-05-12 23:56:58.417688814 +0200
@@ -1,5 +1,5 @@
 \unset ECHO
-1..84
+1..97
 ok 1 - has_trigger(schema, table, trigger, desc) should pass
 ok 2 - has_trigger(schema, table, trigger, desc) should have the proper description
 ok 3 - has_trigger(schema, table, trigger, desc) should have the proper diagnostics
@@ -84,3 +84,16 @@
 ok 82 - triggers_are(table, triggers) + extra & missing should fail
 ok 83 - triggers_are(table, triggers) + extra & missing should have the proper description
 ok 84 - triggers_are(table, triggers) + extra & missing should have the proper diagnostics
+ok 85 - trigger_events_are(table, trigger, events, desc) should pass
+ok 86 - trigger_events_are(table, trigger, events, desc) + extra should fail
+ok 87 - trigger_events_are(table, trigger, events, desc) + extra should have the proper description
+ok 88 - trigger_events_are(table, trigger, events, desc) + extra should have the proper diagnostics
+ok 89 - trigger_events_are(table, trigger, events, desc) should fail
+ok 90 - trigger_events_are(table, trigger, events, desc) should have the proper description
+ok 91 - trigger_events_are(table, trigger, events, desc) should have the proper diagnostics
+ok 92 - trigger_events_are(table, trigger, events, desc) + extra & missing should fail
+ok 93 - trigger_events_are(table, trigger, events, desc) + extra & missing should have the proper description
+ok 94 - trigger_events_are(table, trigger, events, desc) + extra & missing should have the proper diagnostics
+ok 95 - trigger_events_are(table, non_existent_trigger, events, desc) should fail
+ok 96 - trigger_events_are(non_existent_table, trigger, events, desc) should fail
+ok 97 - trigger_events_are(table, trigger, ARRAY[], desc) should fail

Running the trigger.sql with pg_prove like following is working:

pg_prove -d regression test/sql/trigger.sql
test/sql/trigger.sql .. ok     
All tests successful.
Files=1, Tests=97,  1 wallclock secs ( 0.19 usr  0.03 sys +  0.35 cusr  0.17 csys =  0.74 CPU)
Result: PASS
theory commented 4 months ago

Yeah you added new tests so you have to update test/expected/trigger.out. This should fix it:

cp results/trigger.out test/expected/trigger.out

Sorry for the delayed reply, was completely absorbed in writing a couple conference presentations for the last few weeks.