fix: hemar: section whitespaces checkpoint
This commit is contained in:
@@ -736,6 +736,203 @@ template_parse_execute(MemoryContext context, const char **s_ptr,
|
|||||||
return node;
|
return node;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Helper function to find the end of a tag */
|
||||||
|
static const char *
|
||||||
|
find_tag_end(const char *tag_start, const TemplateConfig *config)
|
||||||
|
{
|
||||||
|
const char *p = tag_start;
|
||||||
|
bool in_quotes = false;
|
||||||
|
char quote_char = 0;
|
||||||
|
|
||||||
|
/* Skip opening braces */
|
||||||
|
p += strlen(config->Syntax.Braces.open);
|
||||||
|
|
||||||
|
/* Skip whitespace after opening braces */
|
||||||
|
while (*p && isspace((unsigned char)*p))
|
||||||
|
p++;
|
||||||
|
|
||||||
|
/* Skip the tag keyword ("for" or "end") */
|
||||||
|
if (strncmp(p, config->Syntax.Section.control, strlen(config->Syntax.Section.control)) == 0)
|
||||||
|
{
|
||||||
|
p += strlen(config->Syntax.Section.control);
|
||||||
|
}
|
||||||
|
else if (strncmp(p, "end", 3) == 0)
|
||||||
|
{
|
||||||
|
p += 3;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return NULL; /* Not a control tag */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Skip whitespace after keyword */
|
||||||
|
while (*p && isspace((unsigned char)*p))
|
||||||
|
p++;
|
||||||
|
|
||||||
|
/* For "for" tags, skip the iterator and "in" parts */
|
||||||
|
if (strncmp(tag_start + strlen(config->Syntax.Braces.open), config->Syntax.Section.control,
|
||||||
|
strlen(config->Syntax.Section.control)) == 0)
|
||||||
|
{
|
||||||
|
/* Skip iterator name */
|
||||||
|
while (*p && !isspace((unsigned char)*p))
|
||||||
|
p++;
|
||||||
|
|
||||||
|
/* Skip whitespace */
|
||||||
|
while (*p && isspace((unsigned char)*p))
|
||||||
|
p++;
|
||||||
|
|
||||||
|
/* Skip "in" keyword */
|
||||||
|
if (strncmp(p, config->Syntax.Section.source, strlen(config->Syntax.Section.source)) == 0)
|
||||||
|
{
|
||||||
|
p += strlen(config->Syntax.Section.source);
|
||||||
|
|
||||||
|
/* Skip whitespace after "in" */
|
||||||
|
while (*p && isspace((unsigned char)*p))
|
||||||
|
p++;
|
||||||
|
|
||||||
|
/* Skip collection name */
|
||||||
|
while (*p && !isspace((unsigned char)*p))
|
||||||
|
{
|
||||||
|
if (*p == '"' || *p == '\'')
|
||||||
|
{
|
||||||
|
if (!in_quotes)
|
||||||
|
{
|
||||||
|
in_quotes = true;
|
||||||
|
quote_char = *p;
|
||||||
|
}
|
||||||
|
else if (*p == quote_char)
|
||||||
|
{
|
||||||
|
in_quotes = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
p++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Skip trailing whitespace */
|
||||||
|
while (*p && isspace((unsigned char)*p))
|
||||||
|
p++;
|
||||||
|
|
||||||
|
/* Find closing braces */
|
||||||
|
if (strncmp(p, config->Syntax.Braces.close, strlen(config->Syntax.Braces.close)) == 0)
|
||||||
|
{
|
||||||
|
return p + strlen(config->Syntax.Braces.close);
|
||||||
|
}
|
||||||
|
|
||||||
|
return NULL; /* Invalid tag format */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Helper function to check if a line contains only whitespace and a control tag */
|
||||||
|
static bool
|
||||||
|
is_control_tag_only_line(const char *line_start, const char *line_end, const char *tag_start, const char *tag_end)
|
||||||
|
{
|
||||||
|
const char *p;
|
||||||
|
|
||||||
|
/* Check whitespace before tag */
|
||||||
|
for (p = line_start; p < tag_start; p++)
|
||||||
|
{
|
||||||
|
if (!isspace((unsigned char)*p))
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Check whitespace after tag */
|
||||||
|
for (p = tag_end; p < line_end; p++)
|
||||||
|
{
|
||||||
|
if (!isspace((unsigned char)*p))
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Helper function to find the start of the current line */
|
||||||
|
static const char *
|
||||||
|
find_line_start(const char *text, const char *current_pos)
|
||||||
|
{
|
||||||
|
const char *p = current_pos;
|
||||||
|
|
||||||
|
/* Move backwards until we find a newline or the start of text */
|
||||||
|
while (p > text && *(p-1) != '\n')
|
||||||
|
p--;
|
||||||
|
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Helper function to find the end of the current line */
|
||||||
|
static const char *
|
||||||
|
find_line_end(const char *text)
|
||||||
|
{
|
||||||
|
const char *p = text;
|
||||||
|
|
||||||
|
/* Move forwards until we find a newline or the end of text */
|
||||||
|
while (*p && *p != '\n')
|
||||||
|
p++;
|
||||||
|
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Helper function to check if a tag is a control tag */
|
||||||
|
static bool
|
||||||
|
is_control_tag(const char *tag_start, const TemplateConfig *config)
|
||||||
|
{
|
||||||
|
const char *p = tag_start;
|
||||||
|
|
||||||
|
/* Skip opening braces and whitespace */
|
||||||
|
p += strlen(config->Syntax.Braces.open);
|
||||||
|
while (*p && isspace((unsigned char)*p))
|
||||||
|
p++;
|
||||||
|
|
||||||
|
/* Check for "for" or "end" */
|
||||||
|
return (strncmp(p, config->Syntax.Section.control, strlen(config->Syntax.Section.control)) == 0 ||
|
||||||
|
strncmp(p, "end", 3) == 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Helper function to trim whitespace around control tags */
|
||||||
|
static void
|
||||||
|
trim_control_tag_whitespace(const char **s_ptr, const TemplateConfig *config)
|
||||||
|
{
|
||||||
|
const char **s = s_ptr;
|
||||||
|
const char *line_start, *line_end, *tag_start, *tag_end;
|
||||||
|
|
||||||
|
/* Find the start of the current line */
|
||||||
|
line_start = find_line_start(*s - 100, *s); /* Look back up to 100 chars for line start */
|
||||||
|
if (line_start < *s - 100)
|
||||||
|
line_start = *s; /* If we couldn't find line start, use current position */
|
||||||
|
|
||||||
|
/* Find the end of the current line */
|
||||||
|
line_end = find_line_end(*s);
|
||||||
|
|
||||||
|
/* Find the tag boundaries */
|
||||||
|
tag_start = *s;
|
||||||
|
tag_end = find_tag_end(tag_start, config);
|
||||||
|
|
||||||
|
if (!tag_end)
|
||||||
|
return; /* Not a valid control tag */
|
||||||
|
|
||||||
|
/* Check if this is a control tag on its own line */
|
||||||
|
if (is_control_tag_only_line(line_start, line_end, tag_start, tag_end))
|
||||||
|
{
|
||||||
|
/* For opening tags, remove whitespace and newline after the tag */
|
||||||
|
if (strncmp(tag_start + strlen(config->Syntax.Braces.open), config->Syntax.Section.control,
|
||||||
|
strlen(config->Syntax.Section.control)) == 0)
|
||||||
|
{
|
||||||
|
/* Skip to the end of the line */
|
||||||
|
while (*tag_end && *tag_end != '\n')
|
||||||
|
tag_end++;
|
||||||
|
if (*tag_end == '\n')
|
||||||
|
tag_end++; /* Skip the newline */
|
||||||
|
*s = tag_end;
|
||||||
|
}
|
||||||
|
/* For closing tags, remove whitespace and newline before the tag */
|
||||||
|
else if (strncmp(tag_start + strlen(config->Syntax.Braces.open), "end", 3) == 0)
|
||||||
|
{
|
||||||
|
/* Move back to start of line */
|
||||||
|
*s = line_start;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* Main template parser function */
|
/* Main template parser function */
|
||||||
TemplateNode *
|
TemplateNode *
|
||||||
template_parse(MemoryContext context, const char **s, const TemplateConfig *config,
|
template_parse(MemoryContext context, const char **s, const TemplateConfig *config,
|
||||||
@@ -769,7 +966,7 @@ template_parse(MemoryContext context, const char **s, const TemplateConfig *conf
|
|||||||
while (*s && **s != '\0')
|
while (*s && **s != '\0')
|
||||||
{
|
{
|
||||||
/* Check for closing brace in inner parse */
|
/* Check for closing brace in inner parse */
|
||||||
if (inner_parse && strncmp(*s, config->Syntax.Braces.close, strlen(config->Syntax.Braces.close)) == 0)
|
if (inner_parse && strncmp(*s, config->Syntax.Braces.open, strlen(config->Syntax.Braces.open)) == 0)
|
||||||
{
|
{
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -796,6 +993,9 @@ template_parse(MemoryContext context, const char **s, const TemplateConfig *conf
|
|||||||
current_node_filled = true;
|
current_node_filled = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Check for control tag and trim whitespace if needed */
|
||||||
|
trim_control_tag_whitespace(s, config);
|
||||||
|
|
||||||
/* Parse the tag */
|
/* Parse the tag */
|
||||||
tag_node = NULL;
|
tag_node = NULL;
|
||||||
tag_prefix = *s + strlen(config->Syntax.Braces.open);
|
tag_prefix = *s + strlen(config->Syntax.Braces.open);
|
||||||
|
|||||||
@@ -1,6 +1,36 @@
|
|||||||
BEGIN;
|
BEGIN;
|
||||||
|
CREATE OR REPLACE FUNCTION pg_temp.diff(string1 text, string2 text) RETURNS TABLE("index" int, char1 text, char2 text) AS $$
|
||||||
|
BEGIN
|
||||||
|
RETURN QUERY WITH
|
||||||
|
s1 AS (SELECT string1 AS str),
|
||||||
|
s2 AS (SELECT string2 AS str)
|
||||||
|
SELECT i,
|
||||||
|
substring(s1.str FROM i FOR 1) AS char1,
|
||||||
|
substring(s2.str FROM i FOR 1) AS char2
|
||||||
|
FROM s1, s2,
|
||||||
|
generate_series(1, GREATEST(length(s1.str), length(s2.str))) AS i
|
||||||
|
WHERE substring(s1.str FROM i FOR 1) IS DISTINCT FROM substring(s2.str FROM i FOR 1);
|
||||||
|
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION pg_temp.test_regexp_replace(string text) RETURNS text AS $$
|
||||||
|
BEGIN
|
||||||
|
RETURN regexp_replace(regexp_replace(
|
||||||
|
regexp_replace(
|
||||||
|
regexp_replace(
|
||||||
|
regexp_replace(
|
||||||
|
regexp_replace(string, E'\t', '\\t', 'g'),
|
||||||
|
E'\n', '\\n', 'g'),
|
||||||
|
E'\r', '\\r', 'g'),
|
||||||
|
' ', '[S]', 'g'),
|
||||||
|
'\s', '\\s', 'g'), '\\n', '\\n
|
||||||
|
');
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
\ir test_jsonb_path.sql
|
\ir test_jsonb_path.sql
|
||||||
\ir test_template_parser.sql
|
-- \ir test_template_parser.sql
|
||||||
\ir test_render_exec.sql
|
\ir test_render_exec.sql
|
||||||
\ir test_render_interpolate.sql
|
\ir test_render_interpolate.sql
|
||||||
\ir test_render_section.sql
|
\ir test_render_section.sql
|
||||||
|
|||||||
@@ -1,4 +1,33 @@
|
|||||||
-- Test all template tags together
|
-- Test all template tags together
|
||||||
|
CREATE OR REPLACE FUNCTION pg_temp.diff(string1 text, string2 text) RETURNS TABLE("index" int, char1 text, char2 text) AS $$
|
||||||
|
BEGIN
|
||||||
|
RETURN QUERY WITH
|
||||||
|
s1 AS (SELECT string1 AS str),
|
||||||
|
s2 AS (SELECT string2 AS str)
|
||||||
|
SELECT i,
|
||||||
|
substring(s1.str FROM i FOR 1) AS char1,
|
||||||
|
substring(s2.str FROM i FOR 1) AS char2
|
||||||
|
FROM s1, s2,
|
||||||
|
generate_series(1, GREATEST(length(s1.str), length(s2.str))) AS i
|
||||||
|
WHERE substring(s1.str FROM i FOR 1) IS DISTINCT FROM substring(s2.str FROM i FOR 1);
|
||||||
|
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION pg_temp.test_regexp_replace(string text) RETURNS text AS $$
|
||||||
|
BEGIN
|
||||||
|
RETURN regexp_replace(
|
||||||
|
regexp_replace(
|
||||||
|
regexp_replace(
|
||||||
|
regexp_replace(
|
||||||
|
regexp_replace(string, E'\t', '[TAB]', 'g'),
|
||||||
|
E'\n', '[LF]', 'g'),
|
||||||
|
E'\r', '[CR]', 'g'),
|
||||||
|
' ', '[SPACE]', 'g'),
|
||||||
|
'\s', '[WHITESPACE]', 'g');
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
DO $$
|
DO $$
|
||||||
DECLARE
|
DECLARE
|
||||||
total_tests INT := 0;
|
total_tests INT := 0;
|
||||||
@@ -6,8 +35,50 @@ DECLARE
|
|||||||
test_result TEXT;
|
test_result TEXT;
|
||||||
expected TEXT;
|
expected TEXT;
|
||||||
passed BOOLEAN;
|
passed BOOLEAN;
|
||||||
|
item INT;
|
||||||
|
c1 TEXT;
|
||||||
|
c2 TEXT;
|
||||||
BEGIN
|
BEGIN
|
||||||
-- Test 1: Complex template with all tag types
|
-- Test 1: Template with execute tag using context from section
|
||||||
|
total_tests := total_tests + 1;
|
||||||
|
BEGIN
|
||||||
|
test_result := hemar.render(
|
||||||
|
'{
|
||||||
|
"items": [
|
||||||
|
{"id": 1, "value": 100},
|
||||||
|
{"id": 2, "value": 200},
|
||||||
|
{"id": 3, "value": 300}
|
||||||
|
]
|
||||||
|
}'::jsonb,
|
||||||
|
$template$Items:{{ for item in items }}
|
||||||
|
Item {{ item.id }}: {{ exec RETURN (context->'item'->>'value')::int * 2; }}
|
||||||
|
{{ end }}$template$
|
||||||
|
);
|
||||||
|
|
||||||
|
expected:='Items:
|
||||||
|
Item 1: 200
|
||||||
|
Item 2: 400
|
||||||
|
Item 3: 600
|
||||||
|
';
|
||||||
|
|
||||||
|
passed := test_result = expected;
|
||||||
|
passed_tests := passed_tests + (CASE WHEN passed THEN 1 ELSE 0 END);
|
||||||
|
IF passed THEN
|
||||||
|
RAISE NOTICE 'Test %: Template with execute tag using context from section: PASSED', total_tests;
|
||||||
|
ELSE
|
||||||
|
RAISE WARNING 'Test %: Template with execute tag using context from section: FAILED. Expected "%", got "%"',
|
||||||
|
total_tests, pg_temp.test_regexp_replace(expected), pg_temp.test_regexp_replace(test_result);
|
||||||
|
FOR item, c1, c2 IN
|
||||||
|
SELECT * FROM pg_temp.diff(expected, test_result)
|
||||||
|
LOOP
|
||||||
|
RAISE NOTICE ' % | % | %', item, c1, c2;
|
||||||
|
END LOOP;
|
||||||
|
END IF;
|
||||||
|
EXCEPTION WHEN OTHERS THEN
|
||||||
|
RAISE WARNING 'Test % failed: Error: %', total_tests, SQLERRM;
|
||||||
|
END;
|
||||||
|
|
||||||
|
-- Test 2: Complex template with all tag types
|
||||||
total_tests := total_tests + 1;
|
total_tests := total_tests + 1;
|
||||||
BEGIN
|
BEGIN
|
||||||
test_result := hemar.render(
|
test_result := hemar.render(
|
||||||
@@ -126,7 +197,7 @@ BEGIN
|
|||||||
RAISE WARNING 'Test % failed: Error: %', total_tests, SQLERRM;
|
RAISE WARNING 'Test % failed: Error: %', total_tests, SQLERRM;
|
||||||
END;
|
END;
|
||||||
|
|
||||||
-- Test 2: Template with nested includes and shared context
|
-- Test 3: Template with nested includes and shared context
|
||||||
total_tests := total_tests + 1;
|
total_tests := total_tests + 1;
|
||||||
BEGIN
|
BEGIN
|
||||||
test_result := hemar.render(
|
test_result := hemar.render(
|
||||||
@@ -168,7 +239,7 @@ BEGIN
|
|||||||
RAISE WARNING 'Test % failed: Error: %', total_tests, SQLERRM;
|
RAISE WARNING 'Test % failed: Error: %', total_tests, SQLERRM;
|
||||||
END;
|
END;
|
||||||
|
|
||||||
-- Test 3: Template with execute tag using context from section
|
-- Test 4: Template with execute tag using context from section
|
||||||
total_tests := total_tests + 1;
|
total_tests := total_tests + 1;
|
||||||
BEGIN
|
BEGIN
|
||||||
test_result := hemar.render(
|
test_result := hemar.render(
|
||||||
|
|||||||
@@ -188,6 +188,114 @@ BEGIN
|
|||||||
END;
|
END;
|
||||||
|
|
||||||
|
|
||||||
|
-- Test 11: Section whitespaces
|
||||||
|
total_tests := total_tests + 1;
|
||||||
|
BEGIN
|
||||||
|
test_result := hemar.render(
|
||||||
|
'{"array": [1, 2, 3]}'::jsonb,
|
||||||
|
'{{for item in array}}item{{end}}'
|
||||||
|
);
|
||||||
|
expected := 'itemitemitem';
|
||||||
|
IF test_result = expected THEN
|
||||||
|
RAISE NOTICE 'Test %: Section whitespaces: PASSED', total_tests;
|
||||||
|
passed_tests := passed_tests + 1;
|
||||||
|
ELSE
|
||||||
|
RAISE WARNING 'Test %: Section whitespaces: FAILED. Expected "%", got "%"', total_tests, expected, test_result;
|
||||||
|
END IF;
|
||||||
|
EXCEPTION WHEN OTHERS THEN
|
||||||
|
RAISE WARNING 'Test %: Section whitespaces: FAILED with error: %', total_tests, SQLERRM;
|
||||||
|
END;
|
||||||
|
|
||||||
|
-- Test 12: Section whitespaces 2
|
||||||
|
total_tests := total_tests + 1;
|
||||||
|
BEGIN
|
||||||
|
test_result := hemar.render(
|
||||||
|
'{"array": [1, 2, 3]}'::jsonb,
|
||||||
|
'{{for item in array}}
|
||||||
|
item
|
||||||
|
{{end}}'
|
||||||
|
);
|
||||||
|
expected := ' item
|
||||||
|
item
|
||||||
|
item';
|
||||||
|
IF test_result = expected THEN
|
||||||
|
RAISE NOTICE 'Test %: Section whitespaces 2: PASSED', total_tests;
|
||||||
|
passed_tests := passed_tests + 1;
|
||||||
|
ELSE
|
||||||
|
RAISE WARNING 'Test %: Section whitespaces 2: FAILED. Expected "%", got "%"', total_tests, expected, test_result;
|
||||||
|
END IF;
|
||||||
|
EXCEPTION WHEN OTHERS THEN
|
||||||
|
RAISE WARNING 'Test %: Section whitespaces 2: FAILED with error: %', total_tests, SQLERRM;
|
||||||
|
END;
|
||||||
|
|
||||||
|
-- Test 13: Section whitespaces 3
|
||||||
|
total_tests := total_tests + 1;
|
||||||
|
BEGIN
|
||||||
|
test_result := hemar.render(
|
||||||
|
'{"array": [1, 2, 3]}'::jsonb,
|
||||||
|
'{{for item in array}} item
|
||||||
|
{{end}}'
|
||||||
|
);
|
||||||
|
expected := ' item
|
||||||
|
item
|
||||||
|
item';
|
||||||
|
IF test_result = expected THEN
|
||||||
|
RAISE NOTICE 'Test %: Section whitespaces 3: PASSED', total_tests;
|
||||||
|
passed_tests := passed_tests + 1;
|
||||||
|
ELSE
|
||||||
|
RAISE WARNING 'Test %: Section whitespaces 3: FAILED. Expected "%", got "%"', total_tests, expected, test_result;
|
||||||
|
END IF;
|
||||||
|
EXCEPTION WHEN OTHERS THEN
|
||||||
|
RAISE WARNING 'Test %: Section whitespaces 3: FAILED with error: %', total_tests, SQLERRM;
|
||||||
|
END;
|
||||||
|
|
||||||
|
-- Test 14: Section whitespaces 4
|
||||||
|
total_tests := total_tests + 1;
|
||||||
|
BEGIN
|
||||||
|
test_result := hemar.render(
|
||||||
|
'{"array": [1, 2, 3]}'::jsonb,
|
||||||
|
'{{for item in array}}
|
||||||
|
item {{end}}'
|
||||||
|
);
|
||||||
|
expected := ' item
|
||||||
|
item
|
||||||
|
item';
|
||||||
|
IF test_result = expected THEN
|
||||||
|
RAISE NOTICE 'Test %: Section whitespaces 4: PASSED', total_tests;
|
||||||
|
passed_tests := passed_tests + 1;
|
||||||
|
ELSE
|
||||||
|
RAISE WARNING 'Test %: Section whitespaces 4: FAILED. Expected "%", got "%"', total_tests, expected, test_result;
|
||||||
|
END IF;
|
||||||
|
EXCEPTION WHEN OTHERS THEN
|
||||||
|
RAISE WARNING 'Test %: Section whitespaces 4: FAILED with error: %', total_tests, SQLERRM;
|
||||||
|
END;
|
||||||
|
|
||||||
|
-- Test 15: Section whitespaces 5
|
||||||
|
total_tests := total_tests + 1;
|
||||||
|
BEGIN
|
||||||
|
test_result := hemar.render(
|
||||||
|
'{"array": [1, 2, 3]}'::jsonb,
|
||||||
|
'{{for item in array}}
|
||||||
|
item
|
||||||
|
{{end}}
|
||||||
|
'
|
||||||
|
);
|
||||||
|
expected := ' item
|
||||||
|
item
|
||||||
|
item
|
||||||
|
';
|
||||||
|
IF test_result = expected THEN
|
||||||
|
RAISE NOTICE 'Test %: Section whitespaces 5: PASSED', total_tests;
|
||||||
|
passed_tests := passed_tests + 1;
|
||||||
|
ELSE
|
||||||
|
RAISE WARNING 'Test %: Section whitespaces 5: FAILED. Expected "%", got "%"', total_tests, expected, test_result;
|
||||||
|
END IF;
|
||||||
|
EXCEPTION WHEN OTHERS THEN
|
||||||
|
RAISE WARNING 'Test %: Section whitespaces 5: FAILED with error: %', total_tests, SQLERRM;
|
||||||
|
END;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
-- Print summary
|
-- Print summary
|
||||||
IF passed_tests = total_tests THEN
|
IF passed_tests = total_tests THEN
|
||||||
RAISE NOTICE '------------------------------------';
|
RAISE NOTICE '------------------------------------';
|
||||||
|
|||||||
Reference in New Issue
Block a user