-- -- doctest_check -- execute a query and chekc if it returns the expected value -- ouput is in TAP format -- CREATE OR REPLACE FUNCTION @extschema@.doctest_check( _query TEXT, _want TEXT ) RETURNS TEXT AS $$ DECLARE _have TEXT := NULL; BEGIN -- This function will test itself :) -- -- >>> SELECT @extschema@.doctest_check('SELECT 40 + 2','42') LIKE 'ok%'; -- true -- -- results_eq does not work because the expected value is a TEXT and -- the result of the call may be in a different type --RETURN results_eq(_call,ARRAY[NULLIF(_expected,'NULL')],_call); BEGIN EXECUTE _query INTO _have; EXCEPTION WHEN others THEN NULL; END; RETURN is(COALESCE(_have,'NULL'),_want, _query); END $$ LANGUAGE plpgsql STRICT IMMUTABLE; -- -- doctest_findfuncs -- list all function containing at least one doctest -- CREATE OR REPLACE FUNCTION @extschema@.doctest_findfuncs( _schema NAME DEFAULT NULL, _pattern TEXT DEFAULT NULL ) RETURNS TABLE (oid OID) AS $$ SELECT oid FROM pg_catalog.pg_proc WHERE pronamespace != 'pg_catalog'::REGNAMESPACE AND ( _schema IS NULL OR pronamespace = _schema::REGNAMESPACE::OID ) AND ( _pattern IS NULL OR proname ~ _pattern ) AND pg_catalog.regexp_match(prosrc,E'\\s*--\\s*>>>\\s*(.*)') IS NOT NULL $$ LANGUAGE sql IMMUTABLE; -- -- doctest_parse_query -- extracts the doctest from a comment line -- returns NULL if not found -- CREATE OR REPLACE FUNCTION @extschema@.doctest_parse_query( TEXT ) RETURNS TEXT AS $$ -- -- >>> SELECT @extschema@.doctest_parse_query(NULL); -- NULL -- -- >>> SELECT @extschema@.doctest_parse_query('-- >>> SELECT 40 + 2 '); -- SELECT 40 + 2 -- -- >>> SELECT @extschema@.doctest_parse_query('-- This is not a doctest line'); -- NULL -- SELECT trim( (pg_catalog.regexp_match( $1, E'^\\s*--\\s*>>>\\s*(.*)$') )[1] ); $$ LANGUAGE sql STRICT IMMUTABLE ; -- -- doctest_parse_expected -- extract the expected result from a comment line -- CREATE OR REPLACE FUNCTION @extschema@.doctest_parse_expected( TEXT ) RETURNS TEXT AS $$ -- -- >>> SELECT @extschema@.doctest_parse_expected(NULL); -- NULL -- -- >>> SELECT @extschema@.doctest_parse_expected('-- 42 '); -- 42 -- -- >>> SELECT @extschema@.doctest_parse_expected('This is not a comment'); -- NULL -- SELECT trim((pg_catalog.regexp_match($1,E'\\s*--\\s*(.*)$'))[1]); $$ LANGUAGE sql STRICT IMMUTABLE; -- -- doctest_parse_function_body(_function_id) -- Extract the tests (and expected result) from a given function -- Outputs one line per test -- CREATE OR REPLACE FUNCTION @extschema@.doctest_parse_function_body( _function_id OID ) RETURNS TABLE( query TEXT, -- the test want TEXT -- the expected value ) AS $$ DECLARE codelines TEXT[]; firstline TEXT; nextline TEXT; query TEXT; query_arr TEXT[]; want TEXT; want_arr TEXT[]; i int; j int; BEGIN -- -- This function will test itself ! -- -- >>> SELECT @extschema@.doctest_parse_function_body( -- >>> '@extschema@.doctest_parse_function_body(oid)'::REGPROCEDURE -- >>> ) IS NOT NULL; -- true -- -- 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@.doctest_parse_query(codelines[i]) INTO firstline; IF firstline IS NOT NULL AND i < pg_catalog.array_length(codelines,1) THEN query := 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@.doctest_parse_query(codelines[j]) INTO nextline; WHILE nextline IS NOT NULL AND j < pg_catalog.array_length(codelines,1) LOOP query := pg_catalog.concat(query, ' ', nextline); j := j+1; SELECT @extschema@.doctest_parse_query(codelines[j]) INTO nextline; END LOOP; -- now the next line contains the expected result SELECT @extschema@.doctest_parse_expected(codelines[j]) INTO want; query_arr := pg_catalog.array_append(query_arr,query); want_arr := pg_catalog.array_append(want_arr,want); -- jump to the result line i := j; ELSE i := i+1; END IF; END LOOP; RETURN QUERY SELECT * FROM unnest(query_arr,want_arr); END $$ LANGUAGE plpgsql STRICT IMMUTABLE; -- -- entrypoint for the runtests() function -- CREATE OR REPLACE FUNCTION @extschema@.test() RETURNS SETOF TEXT AS $$ -- By default, ignore all function named `doctest_*` SELECT @extschema@.doctest_runtests(NULL,'^((?!doctest_).)*$'); $$ LANGUAGE SQL IMMUTABLE; -- -- doctest_runtests -- runs all the doctests ! -- CREATE OR REPLACE FUNCTION @extschema@.doctest_runtests( _schema NAME DEFAULT NULL, _pattern TEXT DEFAULT NULL ) RETURNS SETOF TEXT AS $$ DECLARE func RECORD; test RECORD; BEGIN FOR func IN SELECT oid FROM @extschema@.doctest_findfuncs(_schema,_pattern) LOOP RETURN NEXT diag('doctests for '|| func.oid::REGPROCEDURE); FOR test IN SELECT * FROM @extschema@.doctest_parse_function_body(func.oid) LOOP RETURN NEXT @extschema@.doctest_check(test.query,test.want); END LOOP; END LOOP; END $$ LANGUAGE plpgsql IMMUTABLE;