├── 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 | --------------------------------------------------------------------------------