Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 564a328

Browse files
authored
Merge pull request #405 from jgebal/feature/filtering_obj_coll_by_attribute_xpath
Added XPath support for column exclusion in cursor.
2 parents 2a6dcf8 + 5ad40e7 commit 564a328

27 files changed

Lines changed: 421 additions & 159 deletions

docs/userguide/expectations.md

Lines changed: 43 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -211,24 +211,55 @@ end;
211211
```
212212
The `a_nulls_are_equal` parameter controls the behavior of a `null=null` comparison (**this comparison by default is true!**)
213213

214-
### Comparing cursors
214+
### Excluding columns and attributes from comparison
215215

216-
The `equal` matcher accepts an additional parameter `a_exclude varchar2` or `a_exclude ut_varchar2_list`, when used to compare `cursor` data.
216+
The `equal` matcher accepts an additional parameter `a_exclude` when used to compare data of `cursor`, `object` or `table type`.
217217

218-
These parameters enable a list of column names to be passed for exclusion from the data comparison. The list can be a comma separated `varchar2` list or a `ut_varchar2_list` collection.
219-
The column names accepted by the parameter are **case sensitive** and cannot be quoted.
220-
If the `a_exclude` parameter is not specified, all columns are included.
221-
If a column to be excluded does not exist, the column cannot be excluded and it's name is simply ignored.
222-
This is useful when testing cursors containing data that is beyond our control (like default or trigger/procedure generated sysdate values on columns).
218+
This parameter can take three forms:
219+
1. A `varchar2` containing comma separated names of columns/attributes to exclude
220+
2. A `varchar2` containing **XPath** expression that lists items to be excluded
221+
3. A `ut_varchar2_list` containing list of columns/attributes to exclude
222+
223+
The column/attribute names are **case sensitive** and cannot be quoted.
224+
If the `a_exclude` parameter is not specified, whole cursor/object/table type is compared.
225+
If a column/attribute to be excluded does not exist it is simply ignored (no error).
226+
227+
This is useful when testing elements data that cannot be determined or set up by the tests (like `sysdate` populated by default on audit columns).
223228

224229
```sql
225230
procedure test_cursors_skip_columns is
226-
x sys_refcursor;
227-
y sys_refcursor;
231+
l_expected sys_refcursor;
232+
l_actual sys_refcursor;
228233
begin
229-
open x for select 'text' ignore_me, d.* from user_tables d;
230-
open y for select sysdate "ADate", d.* from user_tables d;
231-
ut.expect( a_actual => y ).to_equal( a_expected => x, a_exclude => 'IGNORE_ME,ADate' );
234+
open l_expected for select 'text' ignore_me, d.* from user_tables d;
235+
open l_actual for select sysdate "ADate", d.* from user_tables d;
236+
ut.expect( l_actual ).to_equal( l_expected, a_exclude => 'IGNORE_ME,ADate' );
237+
end;
238+
```
239+
240+
```sql
241+
create or replace type employee as object(
242+
first_name varchar2(50),
243+
last_name varchar2(50),
244+
hire_date date,
245+
created_at timestamp,
246+
created_by varchar2(30),
247+
modified_at timestamp,
248+
modified_by varchar2(50)
249+
);
250+
251+
procedure test_object_skip_columns is
252+
l_expected employee;
253+
l_actual employee;
254+
begin
255+
l_expected := employee('John'||rownum, 'Doe', sysdate, systimestamp, 'me', systimestamp, 'me');
256+
-- the actual should normally be returned by the tested code.
257+
l_actual := employee('John'||rownum, 'Doe', sysdate, systimestamp, 'me', systimestamp, 'me');
258+
259+
-- test the data excluding attributes specified by XPath
260+
ut.expect( anydata.convertObject(l_actual) ).to_equal(
261+
anydata.convertObject(l_expected), a_exclude => '/EMPLOYEE/CREATED_AT|/EMPLOYEE/MODIFIED_AT'
262+
);
232263
end;
233264
```
234265

source/core/ut_annotations.pkb

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,10 @@ create or replace package body ut_annotations as
2727
--c_nonannotat_comment_pattern constant varchar2(30) := '^( |'||chr(09)||')*--+ *[^'||gc_annotation_qualifier||']*?$';
2828
c_comment_replacer_patter constant varchar2(50) := '{COMMENT#%N%}';
2929
c_comment_replacer_regex_ptrn constant varchar2(25) := '{COMMENT#(\d+)}';
30-
c_rgexp_identifier constant varchar2(50) := '[a-z][a-z0-9#_$]*';
30+
c_regexp_identifier constant varchar2(50) := '[a-z][a-z0-9#_$]*';
3131
c_annotation_block_pattern constant varchar2(200) := '(({COMMENT#.+}'||chr(10)||')+)( |'||chr(09)||')*(procedure|function)\s+(' ||
32-
c_rgexp_identifier || ')';
33-
c_annotation_pattern constant varchar2(50) := gc_annotation_qualifier || c_rgexp_identifier || '[ '||chr(9)||']*(\(.*?\)\s*?$)?';
32+
c_regexp_identifier || ')';
33+
c_annotation_pattern constant varchar2(50) := gc_annotation_qualifier || c_regexp_identifier || '[ '||chr(9)||']*(\(.*?\)\s*?$)?';
3434

3535

3636
function delete_multiline_comments(a_source in clob) return clob is
@@ -75,7 +75,7 @@ create or replace package body ut_annotations as
7575

7676
-- get the annotation name and it's parameters if present
7777
l_annotation_name := lower(regexp_substr(l_annotation_str
78-
,'%(' || c_rgexp_identifier || ')'
78+
,'%(' || c_regexp_identifier || ')'
7979
,modifier => 'i'
8080
,subexpression => 1));
8181
l_annotation_params_str := trim(regexp_substr(l_annotation_str, '\((.*?)\)\s*$', subexpression => 1));
@@ -94,7 +94,7 @@ create or replace package body ut_annotations as
9494
,subexpression => 1);
9595

9696
l_param_item.key := regexp_substr(srcstr => l_param_str
97-
,pattern => '(' || c_rgexp_identifier || ')\s*='
97+
,pattern => '(' || c_regexp_identifier || ')\s*='
9898
,modifier => 'i'
9999
,subexpression => 1);
100100
l_param_item.val := trim(regexp_substr(l_param_str, '(.+?=)?(.*$)', subexpression => 2));

source/core/ut_utils.pkb

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -341,5 +341,31 @@ create or replace package body ut_utils is
341341
return l_result;
342342
end;
343343

344+
function to_xpath(a_list varchar2, a_ancestors varchar2 := '/*/') return varchar2 is
345+
l_xpath varchar2(32767) := a_list;
346+
begin
347+
if l_xpath not like '/%' then
348+
l_xpath := to_xpath( clob_to_table(a_clob=>a_list, a_delimiter=>','), a_ancestors);
349+
end if;
350+
return l_xpath;
351+
end;
352+
353+
function to_xpath(a_list ut_varchar2_list, a_ancestors varchar2 := '/*/') return varchar2 is
354+
l_xpath varchar2(32767);
355+
l_item varchar2(32767);
356+
i integer;
357+
begin
358+
i := a_list.first;
359+
while i is not null loop
360+
l_item := trim(a_list(i));
361+
if l_item is not null then
362+
l_xpath := l_xpath || a_ancestors ||a_list(i)||'|';
363+
end if;
364+
i := a_list.next(i);
365+
end loop;
366+
l_xpath := rtrim(l_xpath,',|');
367+
return l_xpath;
368+
end;
369+
344370
end ut_utils;
345371
/

source/core/ut_utils.pks

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ create or replace package ut_utils authid definer is
7979
ex_invalid_rep_event_name exception;
8080
gc_invalid_rep_event_name constant pls_integer := -20211;
8181
pragma exception_init(ex_invalid_rep_event_name, -20211);
82-
82+
8383
-- Any of tests failed
8484
ex_some_tests_failed exception;
8585
gc_some_tests_failed constant pls_integer := -20213;
@@ -219,5 +219,9 @@ create or replace package ut_utils authid definer is
219219

220220
function convert_collection(a_collection ut_varchar2_list) return ut_varchar2_rows;
221221

222+
function to_xpath(a_list varchar2, a_ancestors varchar2 := '/*/') return varchar2;
223+
224+
function to_xpath(a_list ut_varchar2_list, a_ancestors varchar2 := '/*/') return varchar2;
225+
222226
end ut_utils;
223227
/

source/expectations/data_values/ut_data_value_anydata.tpb

Lines changed: 66 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -16,45 +16,99 @@ create or replace type body ut_data_value_anydata as
1616
limitations under the License.
1717
*/
1818

19-
constructor function ut_data_value_anydata(self in out nocopy ut_data_value_anydata) return self as result is
20-
begin
21-
self.self_type := $$plsql_unit;
22-
self.data_type := 'undefined';
23-
return;
24-
end;
25-
2619
overriding member function is_null return boolean is
2720
begin
2821
return true;
2922
end;
3023

3124
overriding member function to_string return varchar2 is
25+
l_result varchar2(32767);
26+
l_clob clob;
3227
begin
33-
return ut_utils.to_string( to_char(null) );
28+
if self.is_null() then
29+
l_result := ut_utils.to_string( to_char(null) );
30+
else
31+
ut_expectation_processor.set_xml_nls_params();
32+
select xmlserialize(content xmltype(self.data_value) indent) into l_clob from dual;
33+
l_result := ut_utils.to_string( l_clob, null );
34+
ut_expectation_processor.reset_nls_params();
35+
end if;
36+
return self.format_multi_line( l_result );
3437
end;
3538

36-
overriding member function compare_implementation( a_other ut_data_value ) return integer is
39+
overriding member function compare_implementation(a_other ut_data_value) return integer is
40+
l_self_data xmltype;
41+
l_other_data xmltype;
42+
l_other ut_data_value_anydata;
43+
l_result integer;
44+
procedure exclude_xpaths(a_xml in out nocopy xmltype, a_xpath varchar2) is
45+
begin
46+
if a_xpath is not null then
47+
select deletexml( a_xml, a_xpath ) into a_xml from dual;
48+
end if;
49+
end;
3750
begin
38-
return null;
51+
if a_other is of (ut_data_value_anydata) then
52+
l_other := treat(a_other as ut_data_value_anydata);
53+
--needed for 11g xe as it fails on constructing XMLTYPE from null ANYDATA
54+
if not self.is_null() and not l_other.is_null() then
55+
ut_expectation_processor.set_xml_nls_params();
56+
l_self_data := xmltype.createxml(self.data_value);
57+
l_other_data := xmltype.createxml(l_other.data_value);
58+
--We use `order member function compare` to do data comparison.
59+
--Therefore, in the `ut_equals` matcher, comparison is done by simply checking
60+
-- `l_result := equal_with_nulls((self.expected = a_actual), a_actual)`
61+
--We cannot guarantee that we will always use `expected = actual ` and not `actual = expected`.
62+
--We should expect the same behaviour regardless of that is the order.
63+
-- This is why we need to coalesce `exclude_xpath` though at most one of them will always be populated
64+
exclude_xpaths(l_self_data, coalesce(self.exclude_xpath, l_other.exclude_xpath));
65+
exclude_xpaths(l_other_data, coalesce(self.exclude_xpath, l_other.exclude_xpath));
66+
ut_expectation_processor.reset_nls_params();
67+
if l_self_data is not null and l_other_data is not null then
68+
l_result := dbms_lob.compare( l_self_data.getclobval(), l_other_data.getclobval() );
69+
end if;
70+
end if;
71+
else
72+
raise value_error;
73+
end if;
74+
return l_result;
3975
end;
4076

41-
static function get_instance(a_data_value anydata) return ut_data_value_anydata is
42-
l_result ut_data_value_anydata := ut_data_value_anydata();
77+
final member procedure init(self in out nocopy ut_data_value_anydata, a_value anydata, a_self_type varchar2) is
78+
begin
79+
self.data_value := a_value;
80+
self.self_type := a_self_type;
81+
self.data_type := case when a_value is not null then lower(a_value.gettypename) else 'undefined' end;
82+
end;
83+
84+
static function get_instance(a_data_value anydata, a_exclude varchar2 := null) return ut_data_value_anydata is
85+
l_result ut_data_value_anydata := ut_data_value_object(null);
4386
l_type anytype;
4487
l_type_code integer;
4588
begin
4689
if a_data_value is not null then
4790
l_type_code := a_data_value.gettype(l_type);
4891
if l_type_code = dbms_types.typecode_object then
4992
l_result := ut_data_value_object(a_data_value);
93+
l_result.exclude_xpath := ut_utils.to_xpath(a_exclude);
5094
elsif l_type_code in (dbms_types.typecode_table, dbms_types.typecode_varray, dbms_types.typecode_namedcollection) then
5195
l_result := ut_data_value_collection(a_data_value);
96+
l_result.exclude_xpath := ut_utils.to_xpath(a_exclude,'/*/*/');
5297
else
5398
raise_application_error(-20000, 'Data type '||a_data_value.gettypename||' in ANYDATA is not supported by utPLSQL');
5499
end if;
55100
end if;
56101
return l_result;
57102
end;
58103

104+
static function get_instance(a_data_value anydata, a_exclude ut_varchar2_list) return ut_data_value_anydata is
105+
l_result ut_data_value_anydata;
106+
l_exclude varchar2(32767);
107+
begin
108+
l_exclude := substr(ut_utils.table_to_clob(a_exclude, ','), 1, 32767);
109+
l_result := ut_data_value_anydata.get_instance(a_data_value, l_exclude );
110+
return l_result;
111+
end;
112+
59113
end;
60114
/

source/expectations/data_values/ut_data_value_anydata.tps

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,16 @@ create or replace type ut_data_value_anydata under ut_data_value(
1616
limitations under the License.
1717
*/
1818
data_value anydata,
19-
constructor function ut_data_value_anydata(self in out nocopy ut_data_value_anydata) return self as result,
19+
/*
20+
var: exclude_xpath
21+
Holds xpath (list of columns) to exclude when comparing object
22+
*/
23+
exclude_xpath varchar2(32767),
2024
overriding member function is_null return boolean,
2125
overriding member function to_string return varchar2,
2226
overriding member function compare_implementation( a_other ut_data_value ) return integer,
23-
static function get_instance(a_data_value anydata) return ut_data_value_anydata
24-
) not final
27+
final member procedure init(self in out nocopy ut_data_value_anydata, a_value anydata, a_self_type varchar2),
28+
static function get_instance(a_data_value anydata, a_exclude varchar2 := null) return ut_data_value_anydata,
29+
static function get_instance(a_data_value anydata, a_exclude ut_varchar2_list) return ut_data_value_anydata
30+
) not final not instantiable
2531
/

source/expectations/data_values/ut_data_value_collection.tpb

Lines changed: 1 addition & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,7 @@ create or replace type body ut_data_value_collection as
1818

1919
constructor function ut_data_value_collection(self in out nocopy ut_data_value_collection, a_value anydata) return self as result is
2020
begin
21-
self.data_value := a_value;
22-
self.self_type := $$plsql_unit;
23-
self.data_type := case when a_value is not null then lower(a_value.gettypename) else 'undefined' end;
21+
self.init(a_value, $$plsql_unit);
2422
return;
2523
end;
2624

@@ -74,45 +72,6 @@ create or replace type body ut_data_value_collection as
7472
end if;
7573
end;
7674

77-
overriding member function to_string return varchar2 is
78-
l_result varchar2(32767);
79-
l_clob clob;
80-
begin
81-
if self.is_null() then
82-
l_result := ut_utils.to_string( to_char(null) );
83-
else
84-
ut_expectation_processor.set_xml_nls_params();
85-
select xmlserialize(content xmltype(self.data_value) indent) into l_clob from dual;
86-
l_result := ut_utils.to_string( l_clob, null );
87-
ut_expectation_processor.reset_nls_params();
88-
end if;
89-
return self.format_multi_line( l_result );
90-
end;
91-
92-
overriding member function compare_implementation(a_other ut_data_value) return integer is
93-
l_self_data xmltype;
94-
l_other_data xmltype;
95-
l_other ut_data_value_collection;
96-
l_result integer;
97-
begin
98-
if a_other is of (ut_data_value_collection) then
99-
l_other := treat(a_other as ut_data_value_collection);
100-
--needed for 11g xe as it fails on constructing XMLTYPE from null ANYDATA
101-
if not self.is_null() and not l_other.is_null() then
102-
ut_expectation_processor.set_xml_nls_params();
103-
l_self_data := xmltype.createxml(self.data_value);
104-
l_other_data := xmltype.createxml(l_other.data_value);
105-
ut_expectation_processor.reset_nls_params();
106-
if l_self_data is not null and l_other_data is not null then
107-
l_result := dbms_lob.compare( l_self_data.getclobval(), l_other_data.getclobval() );
108-
end if;
109-
end if;
110-
else
111-
raise value_error;
112-
end if;
113-
return l_result;
114-
end;
115-
11675
overriding member function is_multi_line return boolean is
11776
begin
11877
return not self.is_null();

source/expectations/data_values/ut_data_value_collection.tps

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,6 @@ create or replace type ut_data_value_collection under ut_data_value_anydata(
1818
constructor function ut_data_value_collection(self in out nocopy ut_data_value_collection, a_value anydata) return self as result,
1919
overriding member function is_null return boolean,
2020
member function is_empty return boolean,
21-
overriding member function to_string return varchar2,
22-
overriding member function is_multi_line return boolean,
23-
overriding member function compare_implementation(a_other ut_data_value) return integer
21+
overriding member function is_multi_line return boolean
2422
)
2523
/

0 commit comments

Comments
 (0)