root/trunk/epic/epic.sql

Revision 34, 32.9 kB (checked in by fumanchu, 3 years ago)

Fixed to work on PG 8.1.

  • Property svn:eol-style set to native
Line 
1/*
2
3Epic.sql -- A framework for unit testing Postgres, especially PL/pgSQL.
4
5The framework consists of functions to both help write tests and help run them.
6
7Installing epic.sql
8===================
9
10Simply execute/import the epic.sql file (this file) into the database you'd
11like to test:
12
13    psql testdb
14    testdb=# \i 'C:\\Python24\\Lib\\site-packages\\sql\\pgtest\\epic.sql'
15    CREATE FUNCTION
16     assert_test_schema
17    --------------------
18     t
19    (1 row)
20
21    DROP FUNCTION
22    CREATE FUNCTION
23    CREATE VIEW
24    CREATE FUNCTION
25    CREATE FUNCTION
26    CREATE FUNCTION
27    CREATE FUNCTION
28    CREATE FUNCTION
29    CREATE FUNCTION
30    CREATE FUNCTION
31
32Then, execute/import any tests you've written (see below):
33
34    testdb=# \i 'C:\\Python24\\Lib\\site-packages\\sql\\pgtest\\test_users.sql'
35    SET
36    CREATE FUNCTION
37    testdb=# \i 'C:\\Python24\\Lib\\site-packages\\sql\\pgtest\\test_transactions.sql'
38    SET
39    CREATE FUNCTION
40    CREATE FUNCTION
41
42
43Writing tests
44=============
45
46Write tests as PL/pgSQL procedures. There are only a couple of things to do
47to ensure they work well with the framework:
48   
49    1. ALWAYS RAISE EXCEPTION at the end of test procs to rollback! Even if
50        the test passes, RAISE EXCEPTION '[OK]'. You may instead PERFORM the
51        Epic functions test.pass(), test.fail(msg), test.todo(msg) and
52        test.skip(msg).
53    2. Put your test in the "test" schema.
54    3. Start the name of your test with "test_".
55        I like "test_[schema]_[target proc]" but do what you like.
56    4. Include a comment of the form "-- module: [module_name]", replacing
57        [module_name] with the (arbitrary) name of the "module". This can
58        then be used with test.run_module().
59
60
61Example test
62------------
63
64CREATE OR REPLACE FUNCTION test.test_inner_set_user_state() RETURNS VOID AS $$
65-- module: test_users
66DECLARE
67  user_id   integer;
68  user_rec  users%ROWTYPE;
69BEGIN
70  <<MAIN>>
71  BEGIN
72    -- Create dummy records
73    INSERT INTO users (login_name) VALUES ('test1') RETURNING user_id INTO user_id;
74   
75    -- Run the proc
76    PERFORM "inner".set_user_state(user_id);
77   
78    -- The proc MUST set users.state to 'active';
79    SELECT INTO user_rec * FROM users WHERE user_id = user_id;
80    PERFORM test.assert_equal(user_rec.state, 'active');
81  END MAIN;
82
83  -- ALWAYS RAISE EXCEPTION at the end of test procs to rollback!
84  PERFORM test.pass();
85END;
86$$ LANGUAGE plpgsql;
87
88
89Assertion functions
90-------------------
91
92Epic.sql includes some functions to make tests easier to write (and shorter).
93The following functions all return void, raising an exception if the assertion
94doesn't hold:
95
96    * test.assert(assertion boolean, msg text): this is the 'catch-all'
97        to assert anything that can be evaluated to a boolean. For example,
98        PERFORM test.assert(substring(a from b), b||" not found in "||a);
99    * test.assert_void(call text)
100    * test.assert_equal(elem_1 anyelement, elem_2 anyelement)
101    * test.assert_not_equal(elem_1 anyelement, elem_2 anyelement)
102    * test.assert_greater_than(elem_1 anyelement, elem_2 anyelement)
103    * test.assert_greater_than_or_equal(elem_1 anyelement, elem_2 anyelement)
104    * test.assert_less_than(elem_1 anyelement, elem_2 anyelement)
105    * test.assert_less_than_or_equal(elem_1 anyelement, elem_2 anyelement)
106   
107    * test.assert_rows(row_1 text, row_2 text):
108        Raises an exception if the SELECT statement row_1 != the SELECT statement row_2.
109    * test.assert_column(call text, expected anyarray[, colname text]):
110        Raises an exception if SELECT colname FROM call != expected (in order).
111   
112    * test.assert_raises(call text, errm text, state text):
113        Raises an exception if call does not raise errm
114        (if provided) or state (if provided).
115
116
117Running tests
118=============
119
120Epic includes a VIEW to manage known tests:
121
122    testdb=# SELECT * FROM test.testnames;
123                         name          |      module
124    -----------------------------------+-------------------
125     test_inner_trans_set_active       | test_transactions
126     test_inner_trans_set_create       | test_transactions
127     test_inner_count_users_by_login   | test_users
128    (3 rows)
129
130Because you made the effort to RAISE EXCEPTION even if the test passes,
131it's safe to run any of these tests directly:
132
133    testdb=# SELECT * FROM test.test_inner_trans_set_create();
134    ERROR:  [OK]
135
136However, if you'd like to run multiple tests together, use the module names
137you created to group them:
138
139    testdb=# SELECT * FROM test.run_module('test_transactions');
140                         name    |      module       | result | errcode | errmsg
141    -----------------------------+-------------------+--------+---------+--------
142     test_inner_trans_set_active | test_transactions | [OK]   |         |
143     test_inner_trans_set_create | test_transactions | [OK]   |         |
144    (2 rows)
145
146As you can see, when you use the test.run_* functions, the results are stored
147in a table (called 'test.results'). If you get busy doing other things, you
148can always read directly from that table to see which tests passed. Note also
149that the test.run_* functions trap the [OK] and other exceptions from each test.
150
151If you want to run all tests without regard to module:
152
153    testdb=# SELECT * FROM test.run_all();
154                         name        |      module       | result | errcode |                             errmsg
155    ---------------------------------+-------------------+--------+---------+---------------------------------------------------------------
156     test_inner_trans_set_active     | test_transactions | [OK]   |         |
157     test_inner_trans_set_create     | test_transactions | [OK]   |         |
158     test_inner_count_users_by_login | test_users        | [FAIL] | 42883   | function inner.count_users_by_login("unknown") does not exist
159    (3 rows)
160
161If you want the complete CONTEXT, etc. for the [FAIL] above, run the test
162directly and you'll get the normal traceback, etc. from PL/pgSQL.
163
164Finally, you can run any test individually via:
165
166    testdb=# SELECT * FROM test.run_test('test_inner_trans_set_create');
167                         name        |      module       | result | errcode |                             errmsg
168    ---------------------------------+-------------------+--------+---------+---------------------------------------------------------------
169     test_inner_trans_set_create     | test_transactions | [OK]   |         |
170    (1 row)
171
172By using run_test instead of running the test function directly, you get
173an [OK] or [FAIL] response instead of an exception. You also get an entry
174in test.results, so if you fix a single test, you can update its success
175without having to run a whole set of tests you didn't modify.
176
177*/
178
179CREATE OR REPLACE FUNCTION _epic_init() RETURNS boolean AS $$
180DECLARE
181  t        text;
182BEGIN
183  SET client_min_messages = warning;
184 
185  BEGIN
186    RAISE EXCEPTION 'ignore me';
187  EXCEPTION WHEN OTHERS THEN
188    BEGIN
189      t := SQLSTATE;
190      t := SQLERRM;
191    EXCEPTION WHEN undefined_column THEN
192      -- PG 8.0 did not have SQLSTATE, SQLERRM available. Epic relies
193      -- on the ability to distinguish one error from another.
194      RAISE EXCEPTION 'Epic requires at least PostgreSQL version 8.1';
195    END;
196  END;
197 
198  BEGIN
199    CREATE SCHEMA test;
200  EXCEPTION WHEN duplicate_schema THEN
201    NULL;
202  END;
203 
204  BEGIN
205    CREATE TABLE test.results (name text PRIMARY KEY, module text,
206                               result text, errcode text, errmsg text,
207                               runtime timestamp with time zone default now());
208  EXCEPTION WHEN duplicate_table THEN
209    NULL;
210  END;
211 
212  RETURN TRUE;
213END;
214$$ LANGUAGE plpgsql;
215
216SELECT * FROM _epic_init();
217DROP FUNCTION _epic_init();
218
219
220CREATE OR REPLACE VIEW test.testnames AS
221  SELECT pg_proc.proname AS name,
222    substring(pg_proc.prosrc from E'--\\s+module[:]\\s+(\\S+)') AS module
223  FROM pg_namespace LEFT JOIN pg_proc
224  ON pg_proc.pronamespace::oid = pg_namespace.oid::oid
225  WHERE pg_namespace.nspname = 'test'
226    AND pg_proc.proname LIKE 'test_%';
227
228
229CREATE OR REPLACE FUNCTION test.statement(call text) RETURNS text AS $$
230-- Returns the given SQL string, prepending "SELECT * FROM " if missing.
231DECLARE
232  result    text;
233BEGIN
234  result := rtrim(call, ';');
235  IF result ~* '^[[:space:]]*(SELECT|EXECUTE)[[:space:]]' THEN
236    return result;
237  ELSIF result ~* '^[[:space:]]*(VALUES)[[:space:]]*\\(' THEN
238    return result;
239  ELSE
240    return 'SELECT * FROM ' || result;
241  END IF;
242END;
243$$ LANGUAGE plpgsql;
244
245
246-------------------------------- global records --------------------------------
247
248
249CREATE OR REPLACE FUNCTION test._ensure_globals() RETURNS boolean AS $$
250-- Creates the global id sequence if it does not already exist.
251BEGIN
252  SET client_min_messages = warning;
253 
254  BEGIN
255    CREATE SEQUENCE test._global_ids;
256  EXCEPTION WHEN duplicate_table THEN
257    NULL;
258  END;
259 
260  RETURN TRUE;
261END;
262$$ LANGUAGE plpgsql;
263SELECT * FROM test._ensure_globals();
264DROP FUNCTION test._ensure_globals();
265
266
267CREATE OR REPLACE FUNCTION test.global(call text, name text) RETURNS text AS $$
268-- Stores the given call's output in a TEMP table, and returns the TEMP table name.
269--
270-- 'call' can be any SELECT, table, view, or procedure that returns records.
271DECLARE
272  tablename      text;
273  creator        text;
274BEGIN
275  IF name IS NULL THEN
276    tablename := '_global_' || nextval('test._global_ids');
277  ELSE
278    tablename := name;
279  END IF;
280 
281  BEGIN
282    EXECUTE 'DROP TABLE ' || tablename;
283  EXCEPTION WHEN undefined_table THEN
284    NULL;
285  END;
286 
287  creator := test.statement(call);
288  EXECUTE 'CREATE TEMP TABLE ' || tablename || ' WITHOUT OIDS AS ' || creator;
289  EXECUTE 'COMMENT ON TABLE ' || tablename || ' IS ' || quote_literal(creator);
290  RETURN tablename;
291END;
292$$ LANGUAGE plpgsql;
293
294CREATE OR REPLACE FUNCTION test.global(call text) RETURNS text AS $$
295  SELECT global FROM test.global($1, NULL);
296$$ LANGUAGE SQL;
297
298CREATE OR REPLACE FUNCTION test.get(p_tablename text, p_offset int) RETURNS record AS $$
299-- Returns a record (the first one, by default) from the given global table.
300--
301-- The returned record includes the following additional attributes:
302--   * __name__ (text): The complete name of the TEMP table. This allows you
303--       to pass g.__name__ to functions that take a 'call text' argument,
304--       such as assert_column, assert_values, and assert_empty (since no
305--       procedural languages support passing records as args).
306DECLARE
307  result         record;
308  rownum         int;
309BEGIN
310  IF p_offset IS NULL OR p_offset < 0 THEN
311    rownum := 0;
312  ELSE
313    rownum := p_offset;
314  END IF;
315 
316  EXECUTE 'SELECT *, NULL::text AS __name__ FROM ' || p_tablename || ' LIMIT 1 OFFSET ' || rownum INTO result;
317 
318  -- We add these values outside the EXECUTE in case the SELECT returned no rows.
319  result.__name__ := p_tablename;
320 
321  RETURN result;
322END;
323$$ LANGUAGE plpgsql;
324
325CREATE OR REPLACE FUNCTION test.get(p_tablename text) RETURNS record AS $$
326DECLARE
327  result         record;
328BEGIN
329  EXECUTE 'SELECT *, NULL::text AS __name__ FROM ' || p_tablename || ' LIMIT 1' INTO result;
330 
331  -- We add these values outside the EXECUTE in case the SELECT returned no rows.
332  result.__name__ := p_tablename;
333 
334  RETURN result;
335END;
336$$ LANGUAGE plpgsql;
337
338
339CREATE OR REPLACE FUNCTION test.constructor(tablename text) RETURNS text AS $$
340-- Return the SQL statement used to construct the given global table.
341  SELECT obj_description(pgc.oid, 'pg_class') FROM pg_class pgc WHERE relname = $1;
342$$ LANGUAGE SQL;
343
344
345CREATE OR REPLACE FUNCTION test.len(tablename text) RETURNS int AS $$
346-- Return the number of rows in the given table.
347DECLARE
348  num    int;
349BEGIN
350  EXECUTE 'SELECT COUNT(*) FROM ' || tablename INTO num;
351  RETURN num;
352END
353$$ LANGUAGE plpgsql;
354
355
356CREATE OR REPLACE FUNCTION test.iter(tablename text) RETURNS text AS $$
357-- Return SQL to retrieve all rows in the given table.
358  SELECT 'SELECT * FROM ' || $1
359$$ LANGUAGE sql;
360
361
362CREATE OR REPLACE FUNCTION test.attributes(tablename text) RETURNS SETOF pg_attribute AS $$
363DECLARE
364  rec      record;
365  seen     int := 0;
366  msg      text;
367BEGIN
368  FOR rec IN
369    SELECT * FROM pg_attribute
370    WHERE attrelid = (SELECT oid FROM pg_class WHERE relname = tablename)
371    -- Exclude system columns
372    AND attnum >= 1
373  LOOP
374    seen := seen + 1;
375    RETURN NEXT rec;
376  END LOOP;
377 
378  IF seen = 0 THEN
379    msg := quote_literal(tablename);
380    RAISE EXCEPTION '% has no attributes.', msg;
381  END IF;
382END;
383$$ LANGUAGE plpgsql;
384
385
386CREATE OR REPLACE FUNCTION test.typename(elem anyelement) RETURNS text AS $$
387-- Return the typename of the given element.
388DECLARE
389  name    text;
390BEGIN
391  CREATE TEMP TABLE _elem_type AS SELECT elem;
392  SELECT INTO name pgt.typname
393    FROM pg_attribute pga LEFT JOIN pg_type pgt ON pga.atttypid = pgt.oid
394    WHERE pga.attrelid = (SELECT oid FROM pg_class WHERE relname = '_elem_type')
395    AND pga.attnum = 1;
396  DROP TABLE _elem_type;
397  RETURN name;
398END;
399$$ LANGUAGE plpgsql;
400
401
402------------------------------ Runners ------------------------------
403
404
405CREATE OR REPLACE FUNCTION test.run_test(testname text) RETURNS test.results AS $$
406-- Runs the named test, stores in test.results, and returns success.
407DECLARE
408  modulename      text;
409  output_record   test.results%ROWTYPE;
410  splitpoint      int;
411BEGIN
412  SELECT module INTO modulename FROM test.testnames WHERE name = testname;
413  DELETE FROM test.results WHERE name = testname;
414 
415  BEGIN
416    -- Allow test.* functions to be referenced without a schema name during this transaction.
417    PERFORM set_config('search_path', 'test, ' || current_setting('search_path'), true);
418    EXECUTE 'SELECT * FROM test.' || testname || '();' INTO output_record;
419    RETURN output_record;
420  EXCEPTION WHEN OTHERS THEN
421    IF SQLSTATE = 'P0001' AND SQLERRM LIKE '[%]%' THEN
422      splitpoint := position(']' in SQLERRM);
423      INSERT INTO test.results (name, module, result, errcode, errmsg)
424        VALUES (testname, modulename, substr(SQLERRM, 1, splitpoint),
425                CASE WHEN SQLERRM LIKE '[FAIL]%' THEN SQLSTATE ELSE '' END,
426                btrim(substr(SQLERRM, splitpoint + 1)));
427      SELECT INTO output_record * FROM test.results WHERE name = testname;
428      RETURN output_record;
429    ELSE
430      INSERT INTO test.results (name, module, result, errcode, errmsg)
431        VALUES (testname, modulename, '[FAIL]', SQLSTATE, SQLERRM);
432      SELECT INTO output_record * FROM test.results WHERE name = testname;
433      RETURN output_record;
434    END IF;
435  END;
436 
437  RAISE EXCEPTION 'Test % did not raise an exception as it should have. Exceptions must ALWAYS be raised in test procedures for rollback.', testname;
438END;
439$$ LANGUAGE plpgsql;
440
441
442CREATE OR REPLACE FUNCTION test.run_module(modulename text) RETURNS SETOF test.results AS $$
443-- Runs all tests in the given module, stores in test.results, and returns results.
444DECLARE
445  testname        record;
446  output_record   test.results%ROWTYPE;
447BEGIN
448  FOR testname IN SELECT name FROM test.testnames WHERE module = modulename ORDER BY name ASC
449  LOOP
450    SELECT INTO output_record * FROM test.run_test(testname);
451    RETURN NEXT output_record;
452  END LOOP;
453END;
454$$ LANGUAGE plpgsql;
455
456
457CREATE OR REPLACE FUNCTION test.run_all() RETURNS SETOF test.results AS $$
458-- Runs all known test functions, stores in test.results, and returns results.
459DECLARE
460  testname record;
461  modulename record;
462  output_record test.results%ROWTYPE;
463BEGIN
464  FOR modulename in SELECT DISTINCT module FROM test.testnames ORDER BY module ASC
465  LOOP
466    FOR testname IN SELECT name FROM test.testnames WHERE module = modulename.module ORDER BY name ASC
467    LOOP
468      SELECT INTO output_record * FROM test.run_test(testname.name);
469      RETURN NEXT output_record;
470    END LOOP;
471  END LOOP;
472  RETURN;
473END;
474$$ LANGUAGE plpgsql;
475
476
477------------------------------ Pass/fail ------------------------------
478
479
480CREATE OR REPLACE FUNCTION test.finish(result text, msg text) RETURNS VOID AS $$
481-- Use this to finish a test. Raises the given result as an exception (for rollback).
482DECLARE
483  fullmsg        text;
484BEGIN
485  fullmsg := '[' || result || ']';
486  IF msg IS NOT NULL THEN
487    fullmsg := fullmsg || ' ' || msg;
488  END IF;
489  RAISE EXCEPTION '%', fullmsg;
490END;
491$$ LANGUAGE plpgsql IMMUTABLE;
492
493
494CREATE OR REPLACE FUNCTION test.pass(msg text) RETURNS VOID AS $$
495-- Use this to finish a successful test. Raises exception '[OK] msg'.
496BEGIN
497  PERFORM test.finish('OK', msg);
498END;
499$$ LANGUAGE plpgsql IMMUTABLE;
500CREATE OR REPLACE FUNCTION test.pass() RETURNS VOID AS $$
501-- Use this to finish a successful test. Raises exception '[OK]'.
502BEGIN
503  PERFORM test.finish('OK', NULL);
504END;
505$$ LANGUAGE plpgsql IMMUTABLE;
506
507
508CREATE OR REPLACE FUNCTION test.fail(msg text) RETURNS VOID AS $$
509-- Use this to finish a failed test. Raises exception '[FAIL] msg'.
510BEGIN
511  PERFORM test.finish('FAIL', msg);
512END;
513$$ LANGUAGE plpgsql IMMUTABLE;
514CREATE OR REPLACE FUNCTION test.fail() RETURNS VOID AS $$
515-- Use this to finish a failed test. Raises exception '[FAIL]'.
516BEGIN
517  PERFORM test.finish('FAIL', NULL);
518END;
519$$ LANGUAGE plpgsql IMMUTABLE;
520
521
522CREATE OR REPLACE FUNCTION test.todo(msg text) RETURNS VOID AS $$
523-- Use this to abort a test as 'todo'. Raises exception '[TODO] msg'.
524BEGIN
525  PERFORM test.finish('TODO', msg);
526END;
527$$ LANGUAGE plpgsql IMMUTABLE;
528CREATE OR REPLACE FUNCTION test.todo() RETURNS VOID AS $$
529-- Use this to abort a test as 'todo'. Raises exception '[TODO]'.
530BEGIN
531  PERFORM test.finish('TODO', NULL);
532END;
533$$ LANGUAGE plpgsql IMMUTABLE;
534
535
536CREATE OR REPLACE FUNCTION test.skip(msg text) RETURNS VOID AS $$
537-- Use this to skip a test. Raises exception '[SKIP] msg'.
538BEGIN
539  PERFORM test.finish('SKIP', msg);
540END;
541$$ LANGUAGE plpgsql IMMUTABLE;
542CREATE OR REPLACE FUNCTION test.skip() RETURNS VOID AS $$
543-- Use this to skip a test. Raises exception '[SKIP]'.
544BEGIN
545  PERFORM test.finish('SKIP', NULL);
546END;
547$$ LANGUAGE plpgsql IMMUTABLE;
548
549
550------------------------------ Assertions ------------------------------
551
552
553CREATE OR REPLACE FUNCTION test.assert_void(call text) RETURNS VOID AS $$
554-- Raises an exception if SELECT * FROM call != void.
555DECLARE
556  retval    text;
557BEGIN
558  EXECUTE test.statement(call) INTO retval;
559  IF retval != '' THEN
560    RAISE EXCEPTION 'Call: ''%'' did not return void. Got ''%'' instead.', call, retval;
561  END IF;
562  RETURN;
563END;
564$$ LANGUAGE plpgsql;
565
566
567CREATE OR REPLACE FUNCTION test.assert(assertion boolean, msg text) RETURNS VOID AS $$
568-- Raises an exception (msg) if assertion is false.
569--
570-- assertion may not be NULL.
571BEGIN
572  IF assertion IS NULL THEN
573    RAISE EXCEPTION 'Assertion test may not be NULL.';
574  END IF;
575 
576  IF NOT assertion THEN
577    RAISE EXCEPTION '%', msg;
578  END IF;
579  RETURN;
580END;
581$$ LANGUAGE plpgsql IMMUTABLE;
582
583
584CREATE OR REPLACE FUNCTION test.assert_equal(elem_1 anyelement, elem_2 anyelement) RETURNS VOID AS $$
585-- Raises an exception if elem_1 is not equal to elem_2.
586--
587-- The two arguments must be of the same type. If they are not,
588-- you will receive "ERROR:  invalid input syntax ..."
589BEGIN
590  IF ((elem_1 IS NULL AND NOT (elem_2 IS NULL)) OR
591      (elem_2 IS NULL AND NOT (elem_1 IS NULL)) OR
592      elem_1 != elem_2) THEN
593    RAISE EXCEPTION '% != %', elem_1, elem_2;
594  END IF;
595  RETURN;
596END;
597$$ LANGUAGE plpgsql IMMUTABLE;
598
599
600CREATE OR REPLACE FUNCTION test.assert_not_equal(elem_1 anyelement, elem_2 anyelement) RETURNS VOID AS $$
601-- Raises an exception if elem_1 is equal to elem_2
602--
603-- The two arguments must be of the same type. If they are not,
604-- you will receive "ERROR:  invalid input syntax ..."
605BEGIN
606  IF ((elem_1 IS NULL AND elem_2 IS NULL) OR elem_1 = elem_2) THEN
607    RAISE EXCEPTION '% = %', elem_1, elem_2;
608  END IF;
609  RETURN;
610END;
611$$ LANGUAGE plpgsql IMMUTABLE;
612
613
614CREATE OR REPLACE FUNCTION test.assert_less_than(elem_1 anyelement, elem_2 anyelement) RETURNS VOID AS $$
615-- Raises an exception if elem_1 >= elem_2
616--
617-- The two arguments must be of the same type. If they are not,
618-- you will receive "ERROR:  invalid input syntax ..."
619BEGIN
620  IF (elem_1 IS NULL or elem_2 IS NULL) THEN
621    RAISE EXCEPTION 'Assertion arguments may not be NULL.';
622  END IF;
623  IF NOT (elem_1 < elem_2) THEN
624    RAISE EXCEPTION '% not < %', elem_1, elem_2;
625  END IF;
626  RETURN;
627END;
628$$ LANGUAGE plpgsql IMMUTABLE;
629
630
631CREATE OR REPLACE FUNCTION test.assert_less_than_or_equal(elem_1 anyelement, elem_2 anyelement) RETURNS VOID AS $$
632-- Raises an exception if elem_1 > elem_2
633--
634-- The two arguments must be of the same type. If they are not,
635-- you will receive "ERROR:  invalid input syntax ..."
636BEGIN
637  IF (elem_1 IS NULL or elem_2 IS NULL) THEN
638    RAISE EXCEPTION 'Assertion arguments may not be NULL.';
639  END IF;
640  IF NOT (elem_1 <= elem_2) THEN
641    RAISE EXCEPTION '% not <= %', elem_1, elem_2;
642  END IF;
643  RETURN;
644END;
645$$ LANGUAGE plpgsql IMMUTABLE;
646
647
648CREATE OR REPLACE FUNCTION test.assert_greater_than(elem_1 anyelement, elem_2 anyelement) RETURNS VOID AS $$
649-- Raises an exception if elem_1 <= elem_2
650--
651-- The two arguments must be of the same type. If they are not,
652-- you will receive "ERROR:  invalid input syntax ..."
653BEGIN
654  IF (elem_1 IS NULL or elem_2 IS NULL) THEN
655    RAISE EXCEPTION 'Assertion arguments may not be NULL.';
656  END IF;
657  IF NOT (elem_1 > elem_2) THEN
658    RAISE EXCEPTION '% not > %', elem_1, elem_2;
659  END IF;
660  RETURN;
661END;
662$$ LANGUAGE plpgsql IMMUTABLE;
663
664
665CREATE OR REPLACE FUNCTION test.assert_greater_than_or_equal(elem_1 anyelement, elem_2 anyelement) RETURNS VOID AS $$
666-- Raises an exception if elem_1 < elem_2
667--
668-- The two arguments must be of the same type. If they are not,
669-- you will receive "ERROR:  invalid input syntax ..."
670BEGIN
671  IF (elem_1 IS NULL or elem_2 IS NULL) THEN
672    RAISE EXCEPTION 'Assertion arguments may not be NULL.';
673  END IF;
674  IF NOT (elem_1 >= elem_2) THEN
675    RAISE EXCEPTION '% not >= %', elem_1, elem_2;
676  END IF;
677  RETURN;
678END;
679$$ LANGUAGE plpgsql IMMUTABLE;
680
681
682CREATE OR REPLACE FUNCTION test.assert_raises(call text, errm text, state text) RETURNS VOID AS $$
683-- Raises an exception if call does not raise errm and/or state.
684--
685-- Example:
686--
687--    PERFORM test.assert_raises('get_transaction_by_id(''a'')', 'Bad argument', NULL);
688--
689-- If errm or state are NULL, that value will not be tested. This allows
690-- you to test by message alone (since the 5-char SQLSTATE values are cryptic),
691-- or trap a range of errors by SQLSTATE without regard for the exact message.
692--
693-- If you don't know the message you want to trap, call this function with
694-- errm = '' and state = ''. The resultant error will tell you the
695-- SQLSTATE and SQLERRM that were raised.
696DECLARE
697  msg       text;
698BEGIN
699  BEGIN
700    EXECUTE test.statement(call);
701  EXCEPTION
702    WHEN OTHERS THEN
703      IF ((state IS NOT NULL AND SQLSTATE != state) OR
704          (errm IS NOT NULL AND SQLERRM != errm)) THEN
705        msg = 'Call: ''' || call || ''' raised ''(' || SQLSTATE || ') ' || SQLERRM || ''' instead of ''(' || state || ') ' || errm || '''.';
706        RAISE EXCEPTION '%', msg;
707      END IF;
708      RETURN;
709  END;
710  msg := 'Call: ' || quote_literal(call) || ' did not raise an error.';
711  RAISE EXCEPTION '%', msg;
712END;
713$$ LANGUAGE plpgsql;
714
715CREATE OR REPLACE FUNCTION test.assert_raises(call text, errm text) RETURNS VOID AS $$
716-- Implicit state version of assert_raises
717BEGIN
718  PERFORM test.assert_raises(call, errm, NULL);
719END;
720$$ LANGUAGE plpgsql;
721
722CREATE OR REPLACE FUNCTION test.assert_raises(call text) RETURNS VOID AS $$
723-- Implicit errm, column version of assert_raises
724BEGIN
725  PERFORM test.assert_raises(call, NULL, NULL);
726END;
727$$ LANGUAGE plpgsql;
728
729
730CREATE OR REPLACE FUNCTION test.assert_rows(call_1 text, call_2 text) RETURNS VOID AS $$
731-- Asserts that two sets of rows have equal values.
732--
733-- Both arguments should be SELECT statements yielding a single row or a set of rows.
734-- Either may also be any table, view, or procedure call that returns records.
735-- It is also common for the second arg to be sans a FROM clause, and simply SELECT values.
736-- Neither source nor expected need to be sorted. Either may include a trailing semicolon.
737--
738-- Example:
739--
740--    PERFORM test.assert_rows('SELECT first, last, city FROM table1',
741--                             'SELECT ''Davy'', ''Crockett'', NULL');
742DECLARE
743  rec     record;
744  s       text;
745  e       text;
746  msg     text;
747BEGIN
748  s := test.statement(call_1);
749  e := test.statement(call_2);
750 
751  FOR rec in EXECUTE s || ' EXCEPT ' || e
752  LOOP
753    RAISE EXCEPTION 'Record: % from: % not found in: %', rec, call_1, call_2;
754  END LOOP;
755 
756  FOR rec in EXECUTE e || ' EXCEPT ' || s
757  LOOP
758    RAISE EXCEPTION 'Record: % from: % not found in: %', rec, call_2, call_1;
759  END LOOP;
760  RETURN;
761END;
762$$ LANGUAGE plpgsql;
763
764
765CREATE OR REPLACE FUNCTION test.assert_column(call text, expected anyarray, colname text) RETURNS VOID AS $$
766-- Raises an exception if SELECT colname FROM call != expected (in order).
767--
768-- If colname is NULL or omitted, the first column of call's output will be used.
769--
770-- 'call' can be any table, view, or procedure that returns records.
771-- 'expected' MUST be an array of the same type as colname. If necessary, cast it using :: to avoid
772-- the error 'type of "record1._assert_column_result" does not match that when preparing the plan'.
773-- TODO: can this function detect the type and cast it for the user?
774--
775-- Example:
776--    PERFORM test.assert_column(
777--      'get_favorite_user_ids(' || user_id || ');',
778--      ARRAY[24, 10074, 87321], 'user_id');
779--
780DECLARE
781  record1       record;
782  record2       record;
783  curs_base     refcursor;
784  curs_expected refcursor;
785  firstname     text;
786  found_1       boolean;
787  lower_bound   int;
788  base_type     text;
789  msg           text;
790BEGIN
791  -- Dump the call output into a temp table
792  IF colname IS NULL THEN
793    -- No colname; instead, create the temp table and then read the catalog
794    -- to grab the name of the first column.
795    EXECUTE 'CREATE TEMPORARY TABLE _test_assert_column_base AS ' || test.statement(call);
796    SELECT INTO firstname a.attname
797      FROM pg_class c LEFT JOIN pg_attribute a ON c.oid = a.attrelid
798      WHERE c.relname = '_test_assert_column_base'
799      -- "The number of the column. Ordinary columns are numbered from 1 up.
800      -- System columns, such as oid, have (arbitrary) negative numbers"
801      AND a.attnum >= 1
802      ORDER BY a.attnum;
803    EXECUTE 'ALTER TABLE _test_assert_column_base RENAME ' || firstname || ' TO _assert_column_result;';
804  ELSE
805    EXECUTE 'CREATE TEMPORARY TABLE _test_assert_column_base AS ' ||
806      'SELECT ' || colname || ' AS _assert_column_result FROM ' || call || ';';
807  END IF;
808  SELECT INTO base_type pgt.typname
809    FROM pg_attribute pga LEFT JOIN pg_type pgt ON pga.atttypid = pgt.oid
810    WHERE pga.attrelid = (SELECT oid FROM pg_class WHERE relname = '_test_assert_column_base')
811    AND pga.attnum = 1;
812  -- Casting to ::name doesn't work so well.
813  IF base_type = 'name' THEN
814    base_type := 'text';
815  END IF;
816 
817  -- Dump the provided array into a temp table
818  -- Use EXECUTE for all statements involving this table so its query plan
819  -- doesn't get cached and re-used (or subsequent calls will fail).
820  EXECUTE 'CREATE TEMPORARY TABLE _test_assert_column_expected (LIKE _test_assert_column_base);';
821  lower_bound = array_lower(expected, 1);
822  IF lower_bound iS NOT NULL THEN
823    FOR i IN lower_bound..array_upper(expected, 1)
824    LOOP
825      IF expected[i] IS NULL THEN
826        EXECUTE 'INSERT INTO _test_assert_column_expected (_assert_column_result) VALUES (NULL);';
827      ELSEIF base_type IN ('text', 'varchar', 'char', 'bytea', 'date', 'timestamp', 'timestamptz', 'time', 'timetz') THEN
828        EXECUTE 'INSERT INTO _test_assert_column_expected (_assert_column_result) VALUES ('
829                || quote_literal(expected[i]) || ');';
830      ELSE
831        EXECUTE 'INSERT INTO _test_assert_column_expected (_assert_column_result) VALUES ('
832                || expected[i] || ');';
833      END IF;
834    END LOOP;
835  END IF;
836 
837  -- Compare the two tables in order.
838  <<TRY>>
839  BEGIN
840    OPEN curs_base FOR EXECUTE 'SELECT * FROM _test_assert_column_base';
841    OPEN curs_expected FOR EXECUTE 'SELECT * FROM _test_assert_column_expected';
842    LOOP
843      FETCH curs_base INTO record1;
844      found_1 := FOUND;
845      FETCH curs_expected INTO record2;
846      IF FOUND THEN
847        IF NOT found_1 THEN
848          PERFORM test.fail('element: ' || record2._assert_column_result || ' not found in call: ' || call);
849        END IF;
850      ELSE
851        IF NOT found_1 THEN
852          EXIT;
853        ELSE
854          PERFORM test.fail('record: ' || record1._assert_column_result || ' not found in array: ' || array_to_string(expected, ', '));
855        END IF;
856      END IF;
857      PERFORM test.assert_equal(record1._assert_column_result, record2._assert_column_result);
858    END LOOP;
859  EXCEPTION WHEN OTHERS THEN
860    DROP TABLE _test_assert_column_base;
861    EXECUTE 'DROP TABLE _test_assert_column_expected';
862    msg := SQLERRM;
863    RAISE EXCEPTION '%', msg;
864  END;
865 
866  CLOSE curs_base;
867  CLOSE curs_expected;
868  DROP TABLE _test_assert_column_base;
869  EXECUTE 'DROP TABLE _test_assert_column_expected';
870  RETURN;
871END;
872$$ LANGUAGE plpgsql;
873
874CREATE OR REPLACE FUNCTION test.assert_column(call text, expected anyarray) RETURNS VOID AS $$
875-- Implicit column version of assert_column
876BEGIN
877  PERFORM test.assert_column(call, expected, NULL);
878END;
879$$ LANGUAGE plpgsql;
880
881
882CREATE OR REPLACE FUNCTION test.assert_values(call_1 text, call_2 text, columns text) RETURNS VOID AS $$
883-- Raises an exception if SELECT columns FROM call_1 != SELECT columns FROM call_2.
884--
885-- Example:
886--    row_1 := obj('get_favorite_user_ids(' || user_id || ')')
887--    PERFORM test.assert_equal(row_1.object, 'users WHERE user_id = 355', 'last_name');
888--
889BEGIN
890  PERFORM test.assert_rows(
891    'SELECT ' || columns || ' FROM ' || call_1,
892    'SELECT ' || columns || ' FROM ' || call_2
893    );
894  RETURN;
895END;
896$$ LANGUAGE plpgsql;
897
898
899CREATE OR REPLACE FUNCTION test.assert_empty(calls text[]) RETURNS VOID AS $$
900-- Raises an exception if the given calls have any rows.
901DECLARE
902  result      bool;
903  failed      text[] DEFAULT '{}'::text[];
904  failed_len  int;
905BEGIN
906  IF array_lower(calls, 1) IS NOT NULL THEN
907    FOR i in array_lower(calls, 1)..array_upper(calls, 1)
908    LOOP
909      EXECUTE 'SELECT EXISTS (' || test.statement(calls[i]) || ');' INTO result;
910      IF result THEN
911        failed := failed || ('"' || btrim(calls[i]) || '"');
912      END IF;
913    END LOOP;
914  END IF;
915 
916  IF array_lower(failed, 1) IS NOT NULL THEN
917    failed_len := (array_upper(failed, 1) - array_lower(failed, 1)) + 1;
918    IF failed_len = 1 THEN
919      PERFORM test.fail('The call ' || array_to_string(failed, ', ') || ' is not empty.');
920    ELSEIF failed_len > 1 THEN
921      PERFORM test.fail('The calls ' || array_to_string(failed, ', ') || ' are not empty.');
922    END IF;
923  END IF;
924  RETURN;
925END;
926$$ LANGUAGE plpgsql;
927
928CREATE OR REPLACE FUNCTION test.assert_empty(call text) RETURNS VOID AS $$
929-- Raises an exception if the given call returns any rows.
930DECLARE
931  result    bool;
932BEGIN
933  EXECUTE 'SELECT EXISTS (' || test.statement(call) || ');' INTO result;
934  IF result THEN
935    PERFORM test.fail('The call "' || call || '" is not empty.');
936  END IF;
937  RETURN;
938END;
939$$ LANGUAGE plpgsql;
940
941
942CREATE OR REPLACE FUNCTION test.assert_not_empty(calls text[]) RETURNS VOID AS $$
943-- Raises an exception if the given calls have no rows.
944DECLARE
945  result      bool;
946  failed      text[];
947  failed_len  int;
948BEGIN
949  IF array_lower(calls, 1) IS NOT NULL THEN
950    FOR i in array_lower(calls, 1)..array_upper(calls, 1)
951    LOOP
952      EXECUTE 'SELECT EXISTS (' || test.statement(calls[i]) || ');' INTO result;
953      IF NOT result THEN
954        failed := failed || ('"' || btrim(calls[i]) || '"');
955      END IF;
956    END LOOP;
957  END IF;
958 
959  IF array_lower(failed, 1) IS NULL THEN
960    -- failed is an empty array (no failures).
961    NULL;
962  ELSE
963    failed_len := (array_upper(failed, 1) - array_lower(failed, 1)) + 1;
964    IF failed_len = 1 THEN
965      PERFORM test.fail('The call ' || array_to_string(failed, ', ') || ' is empty.');
966    ELSEIF failed_len > 1 THEN
967      PERFORM test.fail('The calls ' || array_to_string(failed, ', ') || ' are empty.');
968    END IF;
969  END IF;
970  RETURN;
971END;
972$$ LANGUAGE plpgsql;
973
974CREATE OR REPLACE FUNCTION test.assert_not_empty(call text) RETURNS VOID AS $$
975-- Raises an exception if the given table has no rows.
976DECLARE
977  result    bool;
978BEGIN
979  EXECUTE 'SELECT EXISTS (' || test.statement(call) || ');' INTO result;
980  IF NOT result THEN
981    PERFORM test.fail('The call "' || call || '" is empty.');
982  END IF;
983  RETURN;
984END;
985$$ LANGUAGE plpgsql;
986
987
988CREATE OR REPLACE FUNCTION test.timing(call text, number int) RETURNS interval AS $$
989-- Return an interval encompassing 'number' runs of the given call.
990-- If 'number' is NULL or omitted, it defaults to 1000000.
991DECLARE
992  v_call   text;
993  v_number int;
994  start    timestamp with time zone;
995BEGIN
996  v_call := test.statement(call);
997  v_number := number;
998  IF number IS NULL THEN v_number := 1000000; END IF;
999  -- Mustn't use now() here since that value is fixed for the entire transaction.
1000  -- Also, clock_timestamp isn't available until 8.2 so we use timeofday instead.
1001  start := timeofday()::timestamp;
1002  FOR i IN 1..v_number
1003  LOOP
1004    EXECUTE v_call;
1005  END LOOP;
1006  -- We grab the total clock time outside the loop. It's a toss-up whether the loop
1007  -- overhead outweighs assignment overhead if we accumulated the time inside the loop;
1008  -- therefore I chose "outside" since it makes the whole run faster. ;)
1009  RETURN (timeofday()::timestamp - start);
1010END;
1011$$ LANGUAGE plpgsql;
1012
1013
1014CREATE OR REPLACE FUNCTION test.timing(call text) RETURNS interval AS $$
1015-- Return an interval encompassing 1,000 runs of the given call.
1016BEGIN
1017  RETURN test.timing(call, NULL);
1018END;
1019$$ LANGUAGE plpgsql;
Note: See TracBrowser for help on using the browser.