-- -- match_doctest -- extracts the doctest from a line -- returns NULL if not found -- CREATE OR REPLACE FUNCTION @extschema@.match_doctest( line TEXT ) RETURNS TEXT LANGUAGE SQL IMMUTABLE AS $body$ -- -- >>> SELECT match_doctest(NULL); -- NULL -- -- >>> SELECT match_doctest('-- >>> plop'); -- plop -- SELECT trim( (pg_catalog.regexp_match(line, E'^\\s*--\\s*>>>\\s*(.*)$'))[1] ); $body$; -- -- match_expected -- extract the expected result -- CREATE OR REPLACE FUNCTION @extschema@.match_expected( line TEXT ) RETURNS TEXT LANGUAGE SQL IMMUTABLE AS $body$ -- -- >>> SELECT match_expected(NULL); -- NULL -- -- >>> SELECT match_expected('-- 42 '); -- 42 -- SELECT trim( (pg_catalog.regexp_match(line,E'\\s*--\\s*(.*)$'))[1] ); $body$; -- -- test_call -- launch a multiline SQL statements ("call") -- register the result of the last statement -- rollback to the initial state -- CREATE OR REPLACE FUNCTION @extschema@.test_call( _call TEXT, _expected TEXT ) RETURNS TABLE( call TEXT, passed BOOLEAN ) LANGUAGE plpgsql AS $body$ DECLARE result TEXT := NULL; BEGIN IF _call IS NULL THEN RAISE NOTICE '_call parameter must not be NULL'; RETURN; END IF; BEGIN EXECUTE _call INTO result; RAISE DEBUG 'ROLLBACK'; EXCEPTION WHEN others THEN NULL; END; RAISE DEBUG 'result = %', result; RETURN QUERY SELECT _call, COALESCE(result,'NULL') = _expected; END $body$ ; CREATE OR REPLACE FUNCTION @extschema@.test_call_tap( _call TEXT, _expected TEXT ) RETURNS TEXT LANGUAGE plpgsql AS $body$ DECLARE result TEXT := NULL; BEGIN BEGIN EXECUTE _call INTO result; RAISE DEBUG 'ROLLBACK'; EXCEPTION WHEN others THEN NULL; END; RAISE DEBUG 'result = %', result; RETURN is(COALESCE(result,'NULL'),_expected); END $body$ ; -- -- parse_function(function_id) -- Extract the tests (and expected result) from a given function -- Outputs one line per test -- CREATE OR REPLACE FUNCTION @extschema@.parse_function( function_id OID ) RETURNS TABLE( call TEXT, expected TEXT ) LANGUAGE plpgsql AS $body$ DECLARE codelines TEXT[]; call TEXT = NULL ; firstline TEXT; nextline TEXT; expected TEXT; tests_arr TEXT[]; expected_arr TEXT[]; i int; j int; BEGIN -- split the source code into a text array SELECT pg_catalog.array_agg(r.*) INTO codelines FROM pg_catalog.pg_proc p LEFT JOIN LATERAL pg_catalog.regexp_split_to_table(p.prosrc,E'\\n') r ON TRUE WHERE p.oid = function_id; -- Walk through each line and searh for doctest strings i := pg_catalog.array_lower(codelines, 1); WHILE i < pg_catalog.array_upper(codelines, 1) LOOP SELECT @extschema@.match_doctest(codelines[i]) INTO firstline; IF firstline IS NOT NULL AND i < pg_catalog.array_length(codelines,1) THEN call := firstline; -- we have found a doctest string on this line -- now look for additional doctest strings in the next lines (if this is a multiline doctest) j := i+1; SELECT @extschema@.match_doctest(codelines[j]) INTO nextline; WHILE nextline IS NOT NULL AND j < pg_catalog.array_length(codelines,1) LOOP call := pg_catalog.concat(call, ' ', nextline); j := j+1; SELECT @extschema@.match_doctest(codelines[j]) INTO nextline; END LOOP; RAISE DEBUG 'call=%', call; -- now the next line contains the expected result SELECT @extschema@.match_expected(codelines[j]) INTO expected; RAISE DEBUG 'expected=%', expected; tests_arr := pg_catalog.array_append(tests_arr,call); expected_arr := pg_catalog.array_append(expected_arr,expected); -- jump to the result line i := j; ELSE i := i+1; END IF; END LOOP; RETURN QUERY SELECT * FROM unnest(tests_arr,expected_arr); END $body$ ; -- -- test_function(function_id) -- Tests a given function -- Outputs one line per test -- CREATE OR REPLACE FUNCTION @extschema@.test_function( function_id oid ) RETURNS TABLE( call TEXT, passed BOOLEAN ) LANGUAGE SQL AS $body$ SELECT t.* FROM @extschema@.parse_function(function_id) c JOIN LATERAL @extschema@.test_call(c.call,c.expected) t ON TRUE; $body$ ; -- -- test_schema(_schema) -- Tests all functions in a given schema -- Outputs one line per test -- CREATE OR REPLACE FUNCTION @extschema@.test_schema( _schema NAME DEFAULT 'public' ) RETURNS TABLE ( function_id OID, call TEXT, passed BOOLEAN ) LANGUAGE SQL AS $body$ SELECT p.oid,t.* FROM pg_catalog.pg_proc p JOIN LATERAL @extschema@.test_function(p.oid) t ON TRUE WHERE p.pronamespace=_schema::regnamespace::oid; $body$ ; -- -- test_schema_agg -- Tests all functions in a given schema -- Outputs only one line per function -- CREATE OR REPLACE FUNCTION @extschema@.test_schema_agg( _schema NAME DEFAULT 'public' ) RETURNS TABLE ( function_name REGPROC, passed BIGINT, failed BIGINT ) LANGUAGE SQL AS $body$ SELECT function_id::REGPROC, COUNT(*) FILTER( WHERE passed), COUNT(*) FILTER( WHERE NOT passed) FROM @extschema@.test_schema(_schema) GROUP BY function_id $body$ ; -- -- passing(_schema) -- True if all the tests in the given schema are OK -- CREATE OR REPLACE FUNCTION @extschema@.passing( _schema TEXT ) RETURNS BOOLEAN LANGUAGE SQL AS $body$ SELECT bool_and(passed) FROM @extschema@.test_schema(_schema) $body$ ; -- -- passing() -- if True, all the tests in all schemas are OK -- CREATE OR REPLACE FUNCTION @extschema@.passing() RETURNS BOOLEAN LANGUAGE SQL AS $body$ SELECT bool_and(@extschema@.passing(nsp.nspname)) FROM pg_catalog.pg_namespace nsp WHERE nsp.nspname not in ('information_schema', 'pg_catalog', '@extschema@' ) and nsp.nspname not like 'pg_toast%' AND nsp.nspname not like 'pg_temp_%' $body$ ; -- -- run(_schema) -- Test all functions in a given schema -- CREATE OR REPLACE FUNCTION @extschema@.run( _schema TEXT DEFAULT 'public' ) RETURNS TABLE( function_name REGPROC, passed BIGINT, failed BIGINT ) LANGUAGE SQL AS $body$ SELECT * FROM @extschema@.test_schema_agg(_schema) $body$ ;