├── frontend
└── readme.txt
├── license.txt
├── install
├── 0.uninstall-unit-test.sql
└── 1.install-unit-test.sql
└── readme.md
/frontend/readme.txt:
--------------------------------------------------------------------------------
1 | placeholder
--------------------------------------------------------------------------------
/license.txt:
--------------------------------------------------------------------------------
1 | The PostgreSQL License
2 |
3 | Copyright (c) 2014, Binod Nepal, Mix Open Foundation (http://mixof.org).
4 |
5 | Permission to use, copy, modify, and distribute this software and its documentation
6 | for any purpose, without fee, and without a written agreement is hereby granted,
7 | provided that the above copyright notice and this paragraph and
8 | the following two paragraphs appear in all copies.
9 |
10 | IN NO EVENT SHALL MIX OPEN FOUNDATION BE LIABLE TO ANY PARTY FOR DIRECT, INDIRECT,
11 | SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING LOST PROFITS,
12 | ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF
13 | MIX OPEN FOUNDATION HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
14 |
15 | MIX OPEN FOUNDATION SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING,
16 | BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
17 | FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON AN "AS IS" BASIS,
18 | AND MIX OPEN FOUNDATION HAS NO OBLIGATIONS TO PROVIDE MAINTENANCE, SUPPORT,
19 | UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
--------------------------------------------------------------------------------
/install/0.uninstall-unit-test.sql:
--------------------------------------------------------------------------------
1 |
2 | /********************************************************************************
3 | The PostgreSQL License
4 |
5 | Copyright (c) 2014, Binod Nepal, Mix Open Foundation (http://mixof.org).
6 |
7 | Permission to use, copy, modify, and distribute this software and its documentation
8 | for any purpose, without fee, and without a written agreement is hereby granted,
9 | provided that the above copyright notice and this paragraph and
10 | the following two paragraphs appear in all copies.
11 |
12 | IN NO EVENT SHALL MIX OPEN FOUNDATION BE LIABLE TO ANY PARTY FOR DIRECT, INDIRECT,
13 | SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING LOST PROFITS,
14 | ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF
15 | MIX OPEN FOUNDATION HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
16 |
17 | MIX OPEN FOUNDATION SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING,
18 | BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
19 | FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON AN "AS IS" BASIS,
20 | AND MIX OPEN FOUNDATION HAS NO OBLIGATIONS TO PROVIDE MAINTENANCE, SUPPORT,
21 | UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
22 | ***********************************************************************************/
23 |
24 | DROP SCHEMA IF EXISTS assert CASCADE;
25 | DROP SCHEMA IF EXISTS unit_tests CASCADE;
26 | DROP DOMAIN IF EXISTS public.test_result CASCADE;
27 |
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 | #PostgreSQL Unit Testing Framework (plpgunit)
2 |
3 | Plpgunit started out of curiosity on why a unit testing framework cannot be simple and easy to use. Plpgunit does not require any additional dependencies and is ready to be used on your PostgreSQL Server database.
4 |
5 | #Documentation
6 | Please visit the wiki page.
7 |
8 | # Creating a Plpgunit Unit Test
9 |
10 | A unit test is a plain old function which must:
11 |
12 | * not have any arguments.
13 | * always return "test_result" data type.
14 |
15 | #First Thing First
16 | However you could do that, but there is no need to call each test function manually. The following query automatically invokes all unit tests that have been already created:
17 |
18 | BEGIN TRANSACTION;
19 | SELECT * FROM unit_tests.begin();
20 | ROLLBACK TRANSACTION;
21 |
22 | Remember, if your test(s) does not contain DML statements, there is no need to BEGIN and ROLLBACK transaction.
23 |
24 | #Examples
25 | View documentation for more examples.
26 |
27 | ## Example #1
28 |
29 | DROP FUNCTION IF EXISTS unit_tests.example1();
30 |
31 | CREATE FUNCTION unit_tests.example1()
32 | RETURNS test_result
33 | AS
34 | $$
35 | DECLARE message test_result;
36 | BEGIN
37 | IF 1 = 1 THEN
38 | SELECT assert.fail('This failed intentionally.') INTO message;
39 | RETURN message;
40 | END IF;
41 |
42 | SELECT assert.ok('End of test.') INTO message;
43 | RETURN message;
44 | END
45 | $$
46 | LANGUAGE plpgsql;
47 |
48 | --BEGIN TRANSACTION;
49 | SELECT * FROM unit_tests.begin();
50 | --ROLLBACK TRANSACTION;
51 |
52 | **Will Result in**
53 |
54 | Test completed on : 2013-10-18 19:30:01.543 UTC.
55 | Total test runtime: 19 ms.
56 |
57 | Total tests run : 1.
58 | Passed tests : 0.
59 | Failed tests : 1.
60 |
61 | List of failed tests:
62 | -----------------------------
63 | unit_tests.example1() --> This failed intentionally.
64 |
65 | ## Example #2
66 |
67 | DROP FUNCTION IF EXISTS unit_tests.example2()
68 |
69 | CREATE FUNCTION unit_tests.example2()
70 | RETURNS test_result
71 | AS
72 | $$
73 | DECLARE message test_result;
74 | DECLARE result boolean;
75 | DECLARE have integer;
76 | DECLARE want integer;
77 | BEGIN
78 | want := 100;
79 | SELECT 50 + 49 INTO have;
80 |
81 | SELECT * FROM assert.is_equal(have, want) INTO message, result;
82 |
83 | --Test failed.
84 | IF result = false THEN
85 | RETURN message;
86 | END IF;
87 |
88 | --Test passed.
89 | SELECT assert.ok('End of test.') INTO message;
90 | RETURN message;
91 | END
92 | $$
93 | LANGUAGE plpgsql;
94 |
95 | --BEGIN TRANSACTION;
96 | SELECT * FROM unit_tests.begin();
97 | --ROLLBACK TRANSACTION;
98 |
99 | **Will Result in**
100 |
101 | Test completed on : 2013-10-18 19:47:11.886 UTC.
102 | Total test runtime: 21 ms.
103 |
104 | Total tests run : 2.
105 | Passed tests : 0.
106 | Failed tests : 2.
107 |
108 | List of failed tests:
109 | -----------------------------
110 | unit_tests.example1() --> This failed intentionally.
111 | unit_tests.example2() --> ASSERT IS_EQUAL FAILED.
112 |
113 | Have -> 99
114 | Want -> 100
115 |
116 | ## Example 3
117 |
118 | DROP FUNCTION IF EXISTS unit_tests.example3();
119 |
120 | CREATE FUNCTION unit_tests.example3()
121 | RETURNS test_result
122 | AS
123 | $$
124 | DECLARE message test_result;
125 | DECLARE result boolean;
126 | DECLARE have integer;
127 | DECLARE dont_want integer;
128 | BEGIN
129 | dont_want := 100;
130 | SELECT 50 + 49 INTO have;
131 |
132 | SELECT * FROM assert.is_not_equal(have, dont_want) INTO message, result;
133 |
134 | --Test failed.
135 | IF result = false THEN
136 | RETURN message;
137 | END IF;
138 |
139 | --Test passed.
140 | SELECT assert.ok('End of test.') INTO message;
141 | RETURN message;
142 | END
143 | $$
144 | LANGUAGE plpgsql;
145 |
146 | --BEGIN TRANSACTION;
147 | SELECT * FROM unit_tests.begin();
148 | --ROLLBACK TRANSACTION;
149 |
150 | **Will Result in**
151 |
152 | Test completed on : 2013-10-18 19:48:30.578 UTC.
153 | Total test runtime: 11 ms.
154 |
155 | Total tests run : 3.
156 | Passed tests : 1.
157 | Failed tests : 2.
158 |
159 | List of failed tests:
160 | -----------------------------
161 | unit_tests.example1() --> This failed intentionally.
162 | unit_tests.example2() --> ASSERT IS_EQUAL FAILED.
163 |
164 | Have -> 99
165 | Want -> 100
166 |
167 |
168 | ## Need Contributors for Writing Examples
169 | We need contributors. If you are interested to contribute, let's talk:
170 |
171 | https://www.facebook.com/binod.nirvan/
172 |
173 |
174 | Happy testing!
175 |
--------------------------------------------------------------------------------
/install/1.install-unit-test.sql:
--------------------------------------------------------------------------------
1 | /********************************************************************************
2 | The PostgreSQL License
3 |
4 | Copyright (c) 2014, Binod Nepal, Mix Open Foundation (http://mixof.org).
5 |
6 | Permission to use, copy, modify, and distribute this software and its documentation
7 | for any purpose, without fee, and without a written agreement is hereby granted,
8 | provided that the above copyright notice and this paragraph and
9 | the following two paragraphs appear in all copies.
10 |
11 | IN NO EVENT SHALL MIX OPEN FOUNDATION BE LIABLE TO ANY PARTY FOR DIRECT, INDIRECT,
12 | SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING LOST PROFITS,
13 | ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF
14 | MIX OPEN FOUNDATION HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
15 |
16 | MIX OPEN FOUNDATION SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING,
17 | BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
18 | FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON AN "AS IS" BASIS,
19 | AND MIX OPEN FOUNDATION HAS NO OBLIGATIONS TO PROVIDE MAINTENANCE, SUPPORT,
20 | UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
21 | ***********************************************************************************/
22 |
23 | CREATE SCHEMA IF NOT EXISTS assert;
24 | CREATE SCHEMA IF NOT EXISTS unit_tests;
25 |
26 | DO
27 | $$
28 | BEGIN
29 | IF NOT EXISTS
30 | (
31 | SELECT * FROM pg_type
32 | WHERE
33 | typname ='test_result'
34 | AND
35 | typnamespace =
36 | (
37 | SELECT oid FROM pg_namespace
38 | WHERE nspname ='public'
39 | )
40 | ) THEN
41 | CREATE DOMAIN public.test_result AS text;
42 | END IF;
43 | END
44 | $$
45 | LANGUAGE plpgsql;
46 |
47 |
48 | DROP TABLE IF EXISTS unit_tests.test_details CASCADE;
49 | DROP TABLE IF EXISTS unit_tests.tests CASCADE;
50 | DROP TABLE IF EXISTS unit_tests.dependencies CASCADE;
51 | CREATE TABLE unit_tests.tests
52 | (
53 | test_id SERIAL NOT NULL PRIMARY KEY,
54 | started_on TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT(CURRENT_TIMESTAMP AT TIME ZONE 'UTC'),
55 | completed_on TIMESTAMP WITHOUT TIME ZONE NULL,
56 | total_tests integer NULL DEFAULT(0),
57 | failed_tests integer NULL DEFAULT(0),
58 | skipped_tests integer NULL DEFAULT(0)
59 | );
60 |
61 | CREATE INDEX unit_tests_tests_started_on_inx
62 | ON unit_tests.tests(started_on);
63 |
64 | CREATE INDEX unit_tests_tests_completed_on_inx
65 | ON unit_tests.tests(completed_on);
66 |
67 | CREATE INDEX unit_tests_tests_failed_tests_inx
68 | ON unit_tests.tests(failed_tests);
69 |
70 | CREATE TABLE unit_tests.test_details
71 | (
72 | id BIGSERIAL NOT NULL PRIMARY KEY,
73 | test_id integer NOT NULL REFERENCES unit_tests.tests(test_id),
74 | function_name text NOT NULL,
75 | message text NOT NULL,
76 | ts TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT(CURRENT_TIMESTAMP AT TIME ZONE 'UTC'),
77 | status boolean NOT NULL,
78 | executed boolean NOT NULL
79 | );
80 |
81 | CREATE INDEX unit_tests_test_details_test_id_inx
82 | ON unit_tests.test_details(test_id);
83 |
84 | CREATE INDEX unit_tests_test_details_status_inx
85 | ON unit_tests.test_details(status);
86 |
87 | CREATE TABLE unit_tests.dependencies
88 | (
89 | dependency_id BIGSERIAL NOT NULL PRIMARY KEY,
90 | dependent_ns text,
91 | dependent_function_name text NOT NULL,
92 | depends_on_ns text,
93 | depends_on_function_name text NOT NULL
94 | );
95 |
96 | CREATE INDEX unit_tests_dependencies_dependency_id_inx
97 | ON unit_tests.dependencies(dependency_id);
98 |
99 |
100 | DROP FUNCTION IF EXISTS assert.fail(message text);
101 | CREATE FUNCTION assert.fail(message text)
102 | RETURNS text
103 | AS
104 | $$
105 | BEGIN
106 | IF $1 IS NULL OR trim($1) = '' THEN
107 | message := 'NO REASON SPECIFIED';
108 | END IF;
109 |
110 | RAISE WARNING 'ASSERT FAILED : %', message;
111 | RETURN message;
112 | END
113 | $$
114 | LANGUAGE plpgsql
115 | IMMUTABLE STRICT;
116 |
117 | DROP FUNCTION IF EXISTS assert.pass(message text);
118 | CREATE FUNCTION assert.pass(message text)
119 | RETURNS text
120 | AS
121 | $$
122 | BEGIN
123 | RAISE NOTICE 'ASSERT PASSED : %', message;
124 | RETURN '';
125 | END
126 | $$
127 | LANGUAGE plpgsql
128 | IMMUTABLE STRICT;
129 |
130 | DROP FUNCTION IF EXISTS assert.ok(message text);
131 | CREATE FUNCTION assert.ok(message text)
132 | RETURNS text
133 | AS
134 | $$
135 | BEGIN
136 | RAISE NOTICE 'OK : %', message;
137 | RETURN '';
138 | END
139 | $$
140 | LANGUAGE plpgsql
141 | IMMUTABLE STRICT;
142 |
143 | DROP FUNCTION IF EXISTS assert.is_equal(IN have anyelement, IN want anyelement, OUT message text, OUT result boolean);
144 | CREATE FUNCTION assert.is_equal(IN have anyelement, IN want anyelement, OUT message text, OUT result boolean)
145 | AS
146 | $$
147 | BEGIN
148 | IF($1 IS NOT DISTINCT FROM $2) THEN
149 | message := 'Assert is equal.';
150 | PERFORM assert.ok(message);
151 | result := true;
152 | RETURN;
153 | END IF;
154 |
155 | message := E'ASSERT IS_EQUAL FAILED.\n\nHave -> ' || COALESCE($1::text, 'NULL') || E'\nWant -> ' || COALESCE($2::text, 'NULL') || E'\n';
156 | PERFORM assert.fail(message);
157 | result := false;
158 | RETURN;
159 | END
160 | $$
161 | LANGUAGE plpgsql
162 | IMMUTABLE;
163 |
164 |
165 | DROP FUNCTION IF EXISTS assert.are_equal(VARIADIC anyarray, OUT message text, OUT result boolean);
166 | CREATE FUNCTION assert.are_equal(VARIADIC anyarray, OUT message text, OUT result boolean)
167 | AS
168 | $$
169 | DECLARE count integer=0;
170 | DECLARE total_items bigint;
171 | DECLARE total_rows bigint;
172 | BEGIN
173 | result := false;
174 |
175 | WITH counter
176 | AS
177 | (
178 | SELECT *
179 | FROM explode_array($1) AS items
180 | )
181 | SELECT
182 | COUNT(items),
183 | COUNT(*)
184 | INTO
185 | total_items,
186 | total_rows
187 | FROM counter;
188 |
189 | IF(total_items = 0 OR total_items = total_rows) THEN
190 | result := true;
191 | END IF;
192 |
193 | IF(result AND total_items > 0) THEN
194 | SELECT COUNT(DISTINCT $1[s.i])
195 | INTO count
196 | FROM generate_series(array_lower($1,1), array_upper($1,1)) AS s(i)
197 | ORDER BY 1;
198 |
199 | IF count <> 1 THEN
200 | result := FALSE;
201 | END IF;
202 | END IF;
203 |
204 | IF(NOT result) THEN
205 | message := 'ASSERT ARE_EQUAL FAILED.';
206 | PERFORM assert.fail(message);
207 | RETURN;
208 | END IF;
209 |
210 | message := 'Asserts are equal.';
211 | PERFORM assert.ok(message);
212 | result := true;
213 | RETURN;
214 | END
215 | $$
216 | LANGUAGE plpgsql
217 | IMMUTABLE;
218 |
219 | DROP FUNCTION IF EXISTS assert.is_not_equal(IN already_have anyelement, IN dont_want anyelement, OUT message text, OUT result boolean);
220 | CREATE FUNCTION assert.is_not_equal(IN already_have anyelement, IN dont_want anyelement, OUT message text, OUT result boolean)
221 | AS
222 | $$
223 | BEGIN
224 | IF($1 IS DISTINCT FROM $2) THEN
225 | message := 'Assert is not equal.';
226 | PERFORM assert.ok(message);
227 | result := true;
228 | RETURN;
229 | END IF;
230 |
231 | message := E'ASSERT IS_NOT_EQUAL FAILED.\n\nAlready Have -> ' || COALESCE($1::text, 'NULL') || E'\nDon''t Want -> ' || COALESCE($2::text, 'NULL') || E'\n';
232 | PERFORM assert.fail(message);
233 | result := false;
234 | RETURN;
235 | END
236 | $$
237 | LANGUAGE plpgsql
238 | IMMUTABLE;
239 |
240 | DROP FUNCTION IF EXISTS assert.are_not_equal(VARIADIC anyarray, OUT message text, OUT result boolean);
241 | CREATE FUNCTION assert.are_not_equal(VARIADIC anyarray, OUT message text, OUT result boolean)
242 | AS
243 | $$
244 | DECLARE count integer=0;
245 | DECLARE count_nulls bigint;
246 | BEGIN
247 | SELECT COUNT(*)
248 | INTO count_nulls
249 | FROM explode_array($1) AS items
250 | WHERE items IS NULL;
251 |
252 | SELECT COUNT(DISTINCT $1[s.i]) INTO count
253 | FROM generate_series(array_lower($1,1), array_upper($1,1)) AS s(i)
254 | ORDER BY 1;
255 |
256 | IF(count + count_nulls <> array_upper($1,1) OR count_nulls > 1) THEN
257 | message := 'ASSERT ARE_NOT_EQUAL FAILED.';
258 | PERFORM assert.fail(message);
259 | RESULT := FALSE;
260 | RETURN;
261 | END IF;
262 |
263 | message := 'Asserts are not equal.';
264 | PERFORM assert.ok(message);
265 | result := true;
266 | RETURN;
267 | END
268 | $$
269 | LANGUAGE plpgsql
270 | IMMUTABLE;
271 |
272 |
273 | DROP FUNCTION IF EXISTS assert.is_null(IN anyelement, OUT message text, OUT result boolean);
274 | CREATE FUNCTION assert.is_null(IN anyelement, OUT message text, OUT result boolean)
275 | AS
276 | $$
277 | BEGIN
278 | IF($1 IS NULL) THEN
279 | message := 'Assert is NULL.';
280 | PERFORM assert.ok(message);
281 | result := true;
282 | RETURN;
283 | END IF;
284 |
285 | message := E'ASSERT IS_NULL FAILED. NULL value was expected.\n\n\n';
286 | PERFORM assert.fail(message);
287 | result := false;
288 | RETURN;
289 | END
290 | $$
291 | LANGUAGE plpgsql
292 | IMMUTABLE;
293 |
294 | DROP FUNCTION IF EXISTS assert.is_not_null(IN anyelement, OUT message text, OUT result boolean);
295 | CREATE FUNCTION assert.is_not_null(IN anyelement, OUT message text, OUT result boolean)
296 | AS
297 | $$
298 | BEGIN
299 | IF($1 IS NOT NULL) THEN
300 | message := 'Assert is not NULL.';
301 | PERFORM assert.ok(message);
302 | result := true;
303 | RETURN;
304 | END IF;
305 |
306 | message := E'ASSERT IS_NOT_NULL FAILED. The value is NULL.\n\n\n';
307 | PERFORM assert.fail(message);
308 | result := false;
309 | RETURN;
310 | END
311 | $$
312 | LANGUAGE plpgsql
313 | IMMUTABLE;
314 |
315 | DROP FUNCTION IF EXISTS assert.is_true(IN boolean, OUT message text, OUT result boolean);
316 | CREATE FUNCTION assert.is_true(IN boolean, OUT message text, OUT result boolean)
317 | AS
318 | $$
319 | BEGIN
320 | IF($1) THEN
321 | message := 'Assert is true.';
322 | PERFORM assert.ok(message);
323 | result := true;
324 | RETURN;
325 | END IF;
326 |
327 | message := E'ASSERT IS_TRUE FAILED. A true condition was expected.\n\n\n';
328 | PERFORM assert.fail(message);
329 | result := false;
330 | RETURN;
331 | END
332 | $$
333 | LANGUAGE plpgsql
334 | IMMUTABLE;
335 |
336 | DROP FUNCTION IF EXISTS assert.is_false(IN boolean, OUT message text, OUT result boolean);
337 | CREATE FUNCTION assert.is_false(IN boolean, OUT message text, OUT result boolean)
338 | AS
339 | $$
340 | BEGIN
341 | IF(NOT $1) THEN
342 | message := 'Assert is false.';
343 | PERFORM assert.ok(message);
344 | result := true;
345 | RETURN;
346 | END IF;
347 |
348 | message := E'ASSERT IS_FALSE FAILED. A false condition was expected.\n\n\n';
349 | PERFORM assert.fail(message);
350 | result := false;
351 | RETURN;
352 | END
353 | $$
354 | LANGUAGE plpgsql
355 | IMMUTABLE;
356 |
357 | DROP FUNCTION IF EXISTS assert.is_greater_than(IN x anyelement, IN y anyelement, OUT message text, OUT result boolean);
358 | CREATE FUNCTION assert.is_greater_than(IN x anyelement, IN y anyelement, OUT message text, OUT result boolean)
359 | AS
360 | $$
361 | BEGIN
362 | IF($1 > $2) THEN
363 | message := 'Assert greater than condition is satisfied.';
364 | PERFORM assert.ok(message);
365 | result := true;
366 | RETURN;
367 | END IF;
368 |
369 | message := E'ASSERT IS_GREATER_THAN FAILED.\n\n X : -> ' || COALESCE($1::text, 'NULL') || E'\n is not greater than Y: -> ' || COALESCE($2::text, 'NULL') || E'\n';
370 | PERFORM assert.fail(message);
371 | result := false;
372 | RETURN;
373 | END
374 | $$
375 | LANGUAGE plpgsql
376 | IMMUTABLE;
377 |
378 | DROP FUNCTION IF EXISTS assert.is_less_than(IN x anyelement, IN y anyelement, OUT message text, OUT result boolean);
379 | CREATE FUNCTION assert.is_less_than(IN x anyelement, IN y anyelement, OUT message text, OUT result boolean)
380 | AS
381 | $$
382 | BEGIN
383 | IF($1 < $2) THEN
384 | message := 'Assert less than condition is satisfied.';
385 | PERFORM assert.ok(message);
386 | result := true;
387 | RETURN;
388 | END IF;
389 |
390 | message := E'ASSERT IS_LESS_THAN FAILED.\n\n X : -> ' || COALESCE($1::text, 'NULL') || E'\n is not less than Y: -> ' || COALESCE($2::text, 'NULL') || E'\n';
391 | PERFORM assert.fail(message);
392 | result := false;
393 | RETURN;
394 | END
395 | $$
396 | LANGUAGE plpgsql
397 | IMMUTABLE;
398 |
399 | DROP FUNCTION IF EXISTS assert.function_exists(function_name text, OUT message text, OUT result boolean);
400 | CREATE FUNCTION assert.function_exists(function_name text, OUT message text, OUT result boolean)
401 | AS
402 | $$
403 | BEGIN
404 | IF NOT EXISTS
405 | (
406 | SELECT 1
407 | FROM pg_catalog.pg_namespace n
408 | JOIN pg_catalog.pg_proc p
409 | ON pronamespace = n.oid
410 | WHERE replace(nspname || '.' || proname || '(' || oidvectortypes(proargtypes) || ')', ' ' , '')::text=$1
411 | ) THEN
412 | message := format('The function %s does not exist.', $1);
413 | PERFORM assert.fail(message);
414 |
415 | result := false;
416 | RETURN;
417 | END IF;
418 |
419 | message := format('Ok. The function %s exists.', $1);
420 | PERFORM assert.ok(message);
421 | result := true;
422 | RETURN;
423 | END
424 | $$
425 | LANGUAGE plpgsql;
426 |
427 | DROP FUNCTION IF EXISTS assert.if_functions_compile(VARIADIC _schema_name text[], OUT message text, OUT result boolean);
428 | CREATE OR REPLACE FUNCTION assert.if_functions_compile
429 | (
430 | VARIADIC _schema_name text[],
431 | OUT message text,
432 | OUT result boolean
433 | )
434 | AS
435 | $$
436 | DECLARE all_parameters text;
437 | DECLARE current_function RECORD;
438 | DECLARE current_function_name text;
439 | DECLARE current_type text;
440 | DECLARE current_type_schema text;
441 | DECLARE current_parameter text;
442 | DECLARE functions_count smallint := 0;
443 | DECLARE current_parameters_count int;
444 | DECLARE i int;
445 | DECLARE command_text text;
446 | DECLARE failed_functions text;
447 | BEGIN
448 | FOR current_function IN
449 | SELECT proname, proargtypes, nspname
450 | FROM pg_proc
451 | INNER JOIN pg_namespace
452 | ON pg_proc.pronamespace = pg_namespace.oid
453 | WHERE pronamespace IN
454 | (
455 | SELECT oid FROM pg_namespace
456 | WHERE nspname = ANY($1)
457 | AND nspname NOT IN
458 | (
459 | 'assert', 'unit_tests', 'information_schema'
460 | )
461 | AND proname NOT IN('if_functions_compile')
462 | )
463 | LOOP
464 | current_parameters_count := array_upper(current_function.proargtypes, 1) + 1;
465 |
466 | i := 0;
467 | all_parameters := '';
468 |
469 | LOOP
470 | IF i < current_parameters_count THEN
471 | IF i > 0 THEN
472 | all_parameters := all_parameters || ', ';
473 | END IF;
474 |
475 | SELECT
476 | nspname, typname
477 | INTO
478 | current_type_schema, current_type
479 | FROM pg_type
480 | INNER JOIN pg_namespace
481 | ON pg_type.typnamespace = pg_namespace.oid
482 | WHERE pg_type.oid = current_function.proargtypes[i];
483 |
484 | IF(current_type IN('int4', 'int8', 'numeric', 'integer_strict', 'money_strict','decimal_strict', 'integer_strict2', 'money_strict2','decimal_strict2', 'money','decimal', 'numeric', 'bigint')) THEN
485 | current_parameter := '1::' || current_type_schema || '.' || current_type;
486 | ELSIF(substring(current_type, 1, 1) = '_') THEN
487 | current_parameter := 'NULL::' || current_type_schema || '.' || substring(current_type, 2, length(current_type)) || '[]';
488 | ELSIF(current_type in ('date')) THEN
489 | current_parameter := '''1-1-2000''::' || current_type;
490 | ELSIF(current_type = 'bool') THEN
491 | current_parameter := 'false';
492 | ELSE
493 | current_parameter := '''''::' || quote_ident(current_type_schema) || '.' || quote_ident(current_type);
494 | END IF;
495 |
496 | all_parameters = all_parameters || current_parameter;
497 |
498 | i := i + 1;
499 | ELSE
500 | EXIT;
501 | END IF;
502 | END LOOP;
503 |
504 | BEGIN
505 | current_function_name := quote_ident(current_function.nspname) || '.' || quote_ident(current_function.proname);
506 | command_text := 'SELECT * FROM ' || current_function_name || '(' || all_parameters || ');';
507 |
508 | EXECUTE command_text;
509 | functions_count := functions_count + 1;
510 |
511 | EXCEPTION WHEN OTHERS THEN
512 | IF(failed_functions IS NULL) THEN
513 | failed_functions := '';
514 | END IF;
515 |
516 | IF(SQLSTATE IN('42702', '42704')) THEN
517 | failed_functions := failed_functions || E'\n' || command_text || E'\n' || SQLERRM || E'\n';
518 | END IF;
519 | END;
520 |
521 |
522 | END LOOP;
523 |
524 | IF(failed_functions != '') THEN
525 | message := E'The test if_functions_compile failed. The following functions failed to compile : \n\n' || failed_functions;
526 | result := false;
527 | PERFORM assert.fail(message);
528 | RETURN;
529 | END IF;
530 | END;
531 | $$
532 | LANGUAGE plpgsql
533 | VOLATILE;
534 |
535 | DROP FUNCTION IF EXISTS assert.if_views_compile(VARIADIC _schema_name text[], OUT message text, OUT result boolean);
536 | CREATE FUNCTION assert.if_views_compile
537 | (
538 | VARIADIC _schema_name text[],
539 | OUT message text,
540 | OUT result boolean
541 | )
542 | AS
543 | $$
544 |
545 | DECLARE message test_result;
546 | DECLARE current_view RECORD;
547 | DECLARE current_view_name text;
548 | DECLARE command_text text;
549 | DECLARE failed_views text;
550 | BEGIN
551 | FOR current_view IN
552 | SELECT table_name, table_schema
553 | FROM information_schema.views
554 | WHERE table_schema = ANY($1)
555 | LOOP
556 |
557 | BEGIN
558 | current_view_name := quote_ident(current_view.table_schema) || '.' || quote_ident(current_view.table_name);
559 | command_text := 'SELECT * FROM ' || current_view_name || ' LIMIT 1;';
560 |
561 | RAISE NOTICE '%', command_text;
562 |
563 | EXECUTE command_text;
564 |
565 | EXCEPTION WHEN OTHERS THEN
566 | IF(failed_views IS NULL) THEN
567 | failed_views := '';
568 | END IF;
569 |
570 | failed_views := failed_views || E'\n' || command_text || E'\n' || SQLERRM || E'\n';
571 | END;
572 |
573 |
574 | END LOOP;
575 |
576 | IF(failed_views != '') THEN
577 | message := E'The test if_views_compile failed. The following views failed to compile : \n\n' || failed_views;
578 | result := false;
579 | PERFORM assert.fail(message);
580 | RETURN;
581 | END IF;
582 |
583 | RETURN;
584 | END;
585 | $$
586 | LANGUAGE plpgsql
587 | VOLATILE;
588 |
589 |
590 | DROP FUNCTION IF EXISTS unit_tests.add_dependency(p_dependent text, p_depends_on text);
591 | CREATE FUNCTION unit_tests.add_dependency(p_dependent text, p_depends_on text)
592 | RETURNS void
593 | AS
594 | $$
595 | DECLARE dependent_ns text;
596 | DECLARE dependent_name text;
597 | DECLARE depends_on_ns text;
598 | DECLARE depends_on_name text;
599 | DECLARE arr text[];
600 | BEGIN
601 | IF p_dependent LIKE '%.%' THEN
602 | SELECT regexp_split_to_array(p_dependent, E'\\.') INTO arr;
603 | SELECT arr[1] INTO dependent_ns;
604 | SELECT arr[2] INTO dependent_name;
605 | ELSE
606 | SELECT NULL INTO dependent_ns;
607 | SELECT p_dependent INTO dependent_name;
608 | END IF;
609 | IF p_depends_on LIKE '%.%' THEN
610 | SELECT regexp_split_to_array(p_depends_on, E'\\.') INTO arr;
611 | SELECT arr[1] INTO depends_on_ns;
612 | SELECT arr[2] INTO depends_on_name;
613 | ELSE
614 | SELECT NULL INTO depends_on_ns;
615 | SELECT p_depends_on INTO depends_on_name;
616 | END IF;
617 | INSERT INTO unit_tests.dependencies (dependent_ns, dependent_function_name, depends_on_ns, depends_on_function_name)
618 | VALUES (dependent_ns, dependent_name, depends_on_ns, depends_on_name);
619 | END
620 | $$
621 | LANGUAGE plpgsql
622 | STRICT;
623 |
624 |
625 | DROP FUNCTION IF EXISTS unit_tests.begin(verbosity integer, format text);
626 | CREATE FUNCTION unit_tests.begin(verbosity integer DEFAULT 9, format text DEFAULT '')
627 | RETURNS TABLE(message text, result character(1))
628 | AS
629 | $$
630 | DECLARE this record;
631 | DECLARE _function_name text;
632 | DECLARE _sql text;
633 | DECLARE _failed_dependencies text[];
634 | DECLARE _num_of_test_functions integer;
635 | DECLARE _should_skip boolean;
636 | DECLARE _message text;
637 | DECLARE _error text;
638 | DECLARE _context text;
639 | DECLARE _result character(1);
640 | DECLARE _test_id integer;
641 | DECLARE _status boolean;
642 | DECLARE _total_tests integer = 0;
643 | DECLARE _failed_tests integer = 0;
644 | DECLARE _skipped_tests integer = 0;
645 | DECLARE _list_of_failed_tests text;
646 | DECLARE _list_of_skipped_tests text;
647 | DECLARE _started_from TIMESTAMP WITHOUT TIME ZONE;
648 | DECLARE _completed_on TIMESTAMP WITHOUT TIME ZONE;
649 | DECLARE _delta integer;
650 | DECLARE _ret_val text = '';
651 | DECLARE _verbosity text[] =
652 | ARRAY['debug5', 'debug4', 'debug3', 'debug2', 'debug1', 'log', 'notice', 'warning', 'error', 'fatal', 'panic'];
653 | BEGIN
654 | _started_from := clock_timestamp() AT TIME ZONE 'UTC';
655 |
656 | IF(format='teamcity') THEN
657 | RAISE INFO '##teamcity[testSuiteStarted name=''Plpgunit'' message=''Test started from : %'']', _started_from;
658 | ELSE
659 | RAISE INFO 'Test started from : %', _started_from;
660 | END IF;
661 |
662 | IF($1 > 11) THEN
663 | $1 := 9;
664 | END IF;
665 |
666 | EXECUTE 'SET CLIENT_MIN_MESSAGES TO ' || _verbosity[$1];
667 | RAISE WARNING 'CLIENT_MIN_MESSAGES set to : %' , _verbosity[$1];
668 |
669 | SELECT nextval('unit_tests.tests_test_id_seq') INTO _test_id;
670 |
671 | INSERT INTO unit_tests.tests(test_id)
672 | SELECT _test_id;
673 |
674 | DROP TABLE IF EXISTS temp_test_functions;
675 | CREATE TEMP TABLE temp_test_functions AS
676 | SELECT
677 | nspname AS ns_name,
678 | proname AS function_name,
679 | p.oid as oid
680 | FROM pg_catalog.pg_namespace n
681 | JOIN pg_catalog.pg_proc p
682 | ON pronamespace = n.oid
683 | WHERE
684 | prorettype='test_result'::regtype::oid;
685 |
686 | SELECT count(*) INTO _num_of_test_functions FROM temp_test_functions;
687 |
688 | DROP TABLE IF EXISTS temp_dependency_levels;
689 | CREATE TEMP TABLE temp_dependency_levels AS
690 | WITH RECURSIVE dependency_levels(ns_name, function_name, oid, level) AS (
691 | -- select functions without any dependencies
692 | SELECT ns_name, function_name, tf.oid, 0 as level
693 | FROM temp_test_functions tf
694 | LEFT OUTER JOIN unit_tests.dependencies d ON tf.ns_name = d.dependent_ns AND tf.function_name = d.dependent_function_name
695 | WHERE d.dependency_id IS NULL
696 | UNION
697 | -- add functions which depend on the previous level functions
698 | SELECT d.dependent_ns, d.dependent_function_name, tf.oid, level + 1
699 | FROM dependency_levels dl
700 | JOIN unit_tests.dependencies d ON dl.ns_name = d.depends_on_ns AND dl.function_name LIKE d.depends_on_function_name
701 | JOIN temp_test_functions tf ON d.dependent_ns = tf.ns_name AND d.dependent_function_name = tf.function_name
702 | WHERE level < _num_of_test_functions -- don't follow circles for too long
703 | )
704 | SELECT ns_name, function_name, oid, max(level) as max_level
705 | FROM dependency_levels
706 | GROUP BY ns_name, function_name, oid;
707 |
708 | IF (SELECT count(*) < _num_of_test_functions FROM temp_dependency_levels) THEN
709 | SELECT array_to_string(array_agg(tf.ns_name || '.' || tf.function_name || '()'), ', ')
710 | INTO _error
711 | FROM temp_test_functions tf
712 | LEFT OUTER JOIN temp_dependency_levels dl ON tf.oid = dl.oid
713 | WHERE dl.oid IS NULL;
714 | RAISE EXCEPTION 'Cyclic dependencies detected. Check the following test functions: %', _error;
715 | END IF;
716 |
717 | IF exists(SELECT * FROM temp_dependency_levels WHERE max_level = _num_of_test_functions) THEN
718 | SELECT array_to_string(array_agg(ns_name || '.' || function_name || '()'), ', ')
719 | INTO _error
720 | FROM temp_dependency_levels
721 | WHERE max_level = _num_of_test_functions;
722 | RAISE EXCEPTION 'Cyclic dependencies detected. Check the dependency graph including following test functions: %', _error;
723 | END IF;
724 |
725 | FOR this IN
726 | SELECT ns_name, function_name, max_level
727 | FROM temp_dependency_levels
728 | ORDER BY max_level, oid
729 | LOOP
730 | BEGIN
731 | _status := false;
732 | _total_tests := _total_tests + 1;
733 |
734 | _function_name = this.ns_name|| '.' || this.function_name || '()';
735 |
736 | SELECT array_agg(td.function_name)
737 | INTO _failed_dependencies
738 | FROM unit_tests.dependencies d
739 | JOIN unit_tests.test_details td on td.function_name LIKE d.depends_on_ns || '.' || d.depends_on_function_name || '()'
740 | WHERE d.dependent_ns = this.ns_name AND d.dependent_function_name = this.function_name
741 | AND test_id = _test_id AND status = false;
742 |
743 | SELECT _failed_dependencies IS NOT NULL INTO _should_skip;
744 | IF NOT _should_skip THEN
745 | _sql := 'SELECT ' || _function_name || ';';
746 |
747 | RAISE NOTICE 'RUNNING TEST : %.', _function_name;
748 |
749 | IF(format='teamcity') THEN
750 | RAISE INFO '##teamcity[testStarted name=''%'' message=''%'']', _function_name, _started_from;
751 | ELSE
752 | RAISE INFO 'Running test % : %', _function_name, _started_from;
753 | END IF;
754 |
755 | EXECUTE _sql INTO _message;
756 |
757 | IF _message = '' THEN
758 | _status := true;
759 |
760 | IF(format='teamcity') THEN
761 | RAISE INFO '##teamcity[testFinished name=''%'' message=''%'']', _function_name, clock_timestamp() AT TIME ZONE 'UTC';
762 | ELSE
763 | RAISE INFO 'Passed % : %', _function_name, clock_timestamp() AT TIME ZONE 'UTC';
764 | END IF;
765 | ELSE
766 | IF(format='teamcity') THEN
767 | RAISE INFO '##teamcity[testFailed name=''%'' message=''%'']', _function_name, _message;
768 | RAISE INFO '##teamcity[testFinished name=''%'' message=''%'']', _function_name, clock_timestamp() AT TIME ZONE 'UTC';
769 | ELSE
770 | RAISE INFO 'Test failed % : %', _function_name, _message;
771 | END IF;
772 | END IF;
773 | ELSE
774 | -- skipped test
775 | _status := true;
776 | _message = 'Failed dependencies: ' || array_to_string(_failed_dependencies, ',');
777 | IF(format='teamcity') THEN
778 | RAISE INFO '##teamcity[testSkipped name=''%''] : %', _function_name, clock_timestamp() AT TIME ZONE 'UTC';
779 | ELSE
780 | RAISE INFO 'Skipped % : %', _function_name, clock_timestamp() AT TIME ZONE 'UTC';
781 | END IF;
782 | END IF;
783 |
784 | INSERT INTO unit_tests.test_details(test_id, function_name, message, status, executed, ts)
785 | SELECT _test_id, _function_name, _message, _status, NOT _should_skip, clock_timestamp();
786 |
787 | IF NOT _status THEN
788 | _failed_tests := _failed_tests + 1;
789 | RAISE WARNING 'TEST % FAILED.', _function_name;
790 | RAISE WARNING 'REASON: %', _message;
791 | ELSIF NOT _should_skip THEN
792 | RAISE NOTICE 'TEST % COMPLETED WITHOUT ERRORS.', _function_name;
793 | ELSE
794 | _skipped_tests := _skipped_tests + 1;
795 | RAISE WARNING 'TEST % SKIPPED, BECAUSE A DEPENDENCY FAILED.', _function_name;
796 | END IF;
797 |
798 | EXCEPTION WHEN OTHERS THEN
799 | GET STACKED DIAGNOSTICS _context = PG_EXCEPTION_CONTEXT;
800 | _message := 'ERR: [' || SQLSTATE || ']: ' || SQLERRM || E'\n ' || split_part(_context, E'\n', 1);
801 | INSERT INTO unit_tests.test_details(test_id, function_name, message, status, executed)
802 | SELECT _test_id, _function_name, _message, false, true;
803 |
804 | _failed_tests := _failed_tests + 1;
805 |
806 | RAISE WARNING 'TEST % FAILED.', _function_name;
807 | RAISE WARNING 'REASON: %', _message;
808 |
809 | IF(format='teamcity') THEN
810 | RAISE INFO '##teamcity[testFailed name=''%'' message=''%'']', _function_name, _message;
811 | RAISE INFO '##teamcity[testFinished name=''%'' message=''%'']', _function_name, clock_timestamp() AT TIME ZONE 'UTC';
812 | ELSE
813 | RAISE INFO 'Test failed % : %', _function_name, _message;
814 | END IF;
815 | END;
816 | END LOOP;
817 |
818 | _completed_on := clock_timestamp() AT TIME ZONE 'UTC';
819 | _delta := extract(millisecond from _completed_on - _started_from)::integer;
820 |
821 | UPDATE unit_tests.tests
822 | SET total_tests = _total_tests, failed_tests = _failed_tests, skipped_tests = _skipped_tests, completed_on = _completed_on
823 | WHERE test_id = _test_id;
824 |
825 | IF format='junit' THEN
826 | SELECT
827 | ''||
828 | xmlelement
829 | (
830 | name testsuites,
831 | xmlelement
832 | (
833 | name testsuite,
834 | xmlattributes
835 | (
836 | 'plpgunit' AS name,
837 | t.total_tests AS tests,
838 | t.failed_tests AS failures,
839 | 0 AS errors,
840 | EXTRACT
841 | (
842 | EPOCH FROM t.completed_on - t.started_on
843 | ) AS time
844 | ),
845 | xmlagg
846 | (
847 | xmlelement
848 | (
849 | name testcase,
850 | xmlattributes
851 | (
852 | td.function_name
853 | AS name,
854 | EXTRACT
855 | (
856 | EPOCH FROM td.ts - t.started_on
857 | ) AS time
858 | ),
859 | CASE
860 | WHEN td.status=false
861 | THEN
862 | xmlelement
863 | (
864 | name failure,
865 | td.message
866 | )
867 | END
868 | )
869 | )
870 | )
871 | ) INTO _ret_val
872 | FROM unit_tests.test_details td, unit_tests.tests t
873 | WHERE
874 | t.test_id=_test_id
875 | AND
876 | td.test_id=t.test_id
877 | GROUP BY t.test_id;
878 | ELSE
879 | WITH failed_tests AS
880 | (
881 | SELECT row_number() OVER (ORDER BY id) AS id,
882 | unit_tests.test_details.function_name,
883 | unit_tests.test_details.message
884 | FROM unit_tests.test_details
885 | WHERE test_id = _test_id
886 | AND status= false
887 | )
888 | SELECT array_to_string(array_agg(f.id::text || '. ' || f.function_name || ' --> ' || f.message), E'\n') INTO _list_of_failed_tests
889 | FROM failed_tests f;
890 |
891 | WITH skipped_tests AS
892 | (
893 | SELECT row_number() OVER (ORDER BY id) AS id,
894 | unit_tests.test_details.function_name,
895 | unit_tests.test_details.message
896 | FROM unit_tests.test_details
897 | WHERE test_id = _test_id
898 | AND executed = false
899 | )
900 | SELECT array_to_string(array_agg(s.id::text || '. ' || s.function_name || ' --> ' || s.message), E'\n') INTO _list_of_skipped_tests
901 | FROM skipped_tests s;
902 |
903 | _ret_val := _ret_val || 'Test completed on : ' || _completed_on::text || E' UTC. \nTotal test runtime: ' || _delta::text || E' ms.\n';
904 | _ret_val := _ret_val || E'\nTotal tests run : ' || COALESCE(_total_tests, '0')::text;
905 | _ret_val := _ret_val || E'.\nPassed tests : ' || (COALESCE(_total_tests, '0') - COALESCE(_failed_tests, '0') - COALESCE(_skipped_tests, '0'))::text;
906 | _ret_val := _ret_val || E'.\nFailed tests : ' || COALESCE(_failed_tests, '0')::text;
907 | _ret_val := _ret_val || E'.\nSkipped tests : ' || COALESCE(_skipped_tests, '0')::text;
908 | _ret_val := _ret_val || E'.\n\nList of failed tests:\n' || '----------------------';
909 | _ret_val := _ret_val || E'\n' || COALESCE(_list_of_failed_tests, '')::text;
910 | _ret_val := _ret_val || E'.\n\nList of skipped tests:\n' || '----------------------';
911 | _ret_val := _ret_val || E'\n' || COALESCE(_list_of_skipped_tests, '')::text;
912 | _ret_val := _ret_val || E'\n' || E'End of plpgunit test.\n\n';
913 | END IF;
914 |
915 | IF _failed_tests > 0 THEN
916 | _result := 'N';
917 |
918 | IF(format='teamcity') THEN
919 | RAISE INFO '##teamcity[testStarted name=''Result'']';
920 | RAISE INFO '##teamcity[testFailed name=''Result'' message=''%'']', REPLACE(_ret_val, E'\n', ' |n');
921 | RAISE INFO '##teamcity[testFinished name=''Result'']';
922 | RAISE INFO '##teamcity[testSuiteFinished name=''Plpgunit'' message=''%'']', REPLACE(_ret_val, E'\n', '|n');
923 | ELSE
924 | RAISE INFO '%', _ret_val;
925 | END IF;
926 | ELSE
927 | _result := 'Y';
928 |
929 | IF(format='teamcity') THEN
930 | RAISE INFO '##teamcity[testSuiteFinished name=''Plpgunit'' message=''%'']', REPLACE(_ret_val, E'\n', '|n');
931 | ELSE
932 | RAISE INFO '%', _ret_val;
933 | END IF;
934 | END IF;
935 |
936 | SET CLIENT_MIN_MESSAGES TO notice;
937 |
938 | RETURN QUERY SELECT _ret_val, _result;
939 | END
940 | $$
941 | LANGUAGE plpgsql;
942 |
943 | DROP FUNCTION IF EXISTS unit_tests.begin_junit(verbosity integer);
944 | CREATE FUNCTION unit_tests.begin_junit(verbosity integer DEFAULT 9)
945 | RETURNS TABLE(message text, result character(1))
946 | AS
947 | $$
948 | BEGIN
949 | RETURN QUERY
950 | SELECT * FROM unit_tests.begin($1, 'junit');
951 | END
952 | $$
953 | LANGUAGE plpgsql;
954 |
955 | -- version of begin that will raise if any tests have failed
956 | -- this will cause psql to return nonzeo exit code so the build/script can be halted
957 | CREATE OR REPLACE FUNCTION unit_tests.begin_psql(verbosity integer default 9, format text default '')
958 | RETURNS VOID AS $$
959 | DECLARE
960 | _msg text;
961 | _res character(1);
962 | BEGIN
963 | SELECT * INTO _msg, _res
964 | FROM unit_tests.begin(verbosity, format)
965 | ;
966 | IF(_res != 'Y') THEN
967 | RAISE EXCEPTION 'Tests failed [%]', _msg;
968 | END IF;
969 | END
970 | $$
971 | LANGUAGE plpgsql;
972 |
973 |
--------------------------------------------------------------------------------