

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


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

