

--
--  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;

