how to avoid tSQLt to abort the called proc after raiserror? - tsqlt

In our stored procedures , we like to "return" a specific return value after raising and error using raiserror()
For that to work, we use low severity levels (eg 12), followed by a return value, such as:
create or alter proc dbo.usp_test
as
begin
print '**start of proc'
raiserror( 'error on purpose',12,1);
print '**after the error'
return -3
end
This work perfectly when running that procedure:
declare #RC int
exec #RC = dbo.usp_test
print concat('The return code is ', #RC)
/* output:
**start of proc
Msg 50000, Level 12, State 1, Procedure usp_test, Line 6 [Batch Start Line 12]
error on purpose
**after the error
The return code is -3
*/
However, when that proc is called from a unit test, the behaviour is different, suddenly the execution is stopped after the raiserror:
create schema T_usp_test authorization dbo;
GO
EXECUTE sp_addextendedproperty #name = 'tSQLt.TestClass', #value = 1, #level0type = 'SCHEMA', #level0name = 'T_usp_test'
GO
create or alter proc T_usp_test.[test mytest]
as
begin
exec tSQLt.ExpectException;
declare #RC int;
exec #RC = dbo.usp_test;
print concat('The return code is ', #RC)
end
GO
exec tSQLt.Run 'T_usp_test.[test mytest]'
/* output:
(1 row affected)
**start of proc
+----------------------+
|Test Execution Summary|
+----------------------+
|No|Test Case Name |Dur(ms)|Result |
+--+--------------------------+-------+-------+
|1 |[T_usp_test].[test mytest]| 7|Success|
-----------------------------------------------------------------------------
Test Case Summary: 1 test case(s) executed, 1 succeeded, 0 failed, 0 errored.
-----------------------------------------------------------------------------
*/
So the question:
1) why is the behaviour different and is the proc now suddenly stopping executing at raiserror() ?
2) how can I overcome this ?

The core proc that runs the test is tSQLt.Private_RunTest which runs the test in a TRY...CATCH block. From the docs at https://learn.microsoft.com/en-us/sql/t-sql/language-elements/try-catch-transact-sql?view=sql-server-2017 :
A TRY...CATCH construct catches all execution errors that have a severity higher than 10 that do not close the database connection.
It looks like you've set the severity to 12. Consider lowering the severity of your RAISERROR call and see if that changes the behavior. You could also try wrapping your RAISERROR call in its own TRY...CATCH block.

Related

Multiple operations on a result set in asynchronous ADO

Following code:
class function TDB.FlagSet(ADataSet: TDataSet; AFieldName: string): Boolean;
var
f: TField;
begin
Result := (ADataSet <> nil) and (not ADataSet.IsEmpty()) and (not ADataSet.Eof);
if Result then begin
f := ADataSet.FindField(AFieldName);
Result := (f <> nil) and f.AsBoolean;
end;
end;
sometimes (very rare cases!) produces an error:
Either BOF or EOF is True, or the current record has been deleted. Requested operation requires a current record
but I have just checked for EOF and it was False!
This started to happen in TDBGrid.OnDrawColumnCell after I switched the stored procedure caller to [eoAsyncExecute, eoAsyncFetch, eoAsyncFetchNonBlocking].
How to assure consistent result set state in TDB.FlagSet function?

How do I enforce that a type hold only be a fixed set of non-contiguous values?

I was expecting this program to raise an error when I feed it 3 as a valid Scale value, but no such luck:
with Ada.Text_IO; use Ada.Text_IO;
procedure predicate is
type Scale is new Integer
with Dynamic_Predicate => Scale in 1 | 2 | 4 | 8;
GivesWarning : Scale := 3; -- gives warning
begin
Put_Line ("Hello World");
loop
Put_Line ("Gimme a value");
declare
AnyValue : Integer := Integer'Value (Get_Line);
S : Scale := Scale (AnyValue); -- no check done!
begin
Put_Line ("okay, that works" & S'Image);
end;
end loop;
end predicate;
I found this related question, but there the requirement is to use an enum., and the solution is to define an array from enum -> value.
I want something that gives me at least a warning at compile time, and allows me to check at runtime as well, and that raises an error if I try to put an invalid value in. Then, if I can use SPARK to prove that no invalid values can occur, I could turn off said checks. I was under the impression that this was how Static_ / Dynamic_ predicates work, so the example above took me by surprise.
You need to enable assertions. Either compile with -gnata or set an appropriate Assertion_Policy
pragma Assertion_Policy(Dynamic_Predicate => Check);

Starting tasks again

I recently started Ada programming and now I'm stuck.
I created a program with multiple tasks. The main-task is managing incoming communication and as a consequence starts working-tasks or transfers data to the working-tasks.
The working-tasks are all of the same kind but with different identifiers.
They do their work and should finish after that. For example:
task body Access_Protected is
begin
accept Start(foo: in Integer; foo2: out Integer)
do something
end Start;
while Go_loop loop
select
accept Quit do
Go_loop := false;
end Quit;
or
accept Insert(foo3: in Integer)
do something
if something = 0 then
Go_loop := false;
end if;
end Insert;
or delay 2.0;
end select;
end loop;
end Access_Protected;
I understand that the working-task should be terminated when the Go_loop is finished. Am I right?
It works to start the task one time but when the main-task tries to restart the working-task by calling the Start procedure, nothing happens.
Can someone please tell me which point I am missing.
A task and subprogram are somewhat related in that when the body is completed the construct ends, this is to say that the construct ends with it's appropriate end; in the case of a procedure control returns to the caller, in the case of a function the exception PROGRAM_ERROR is raised, and in the case of a task the controlling "thread" terminates.
What's happening in your particular problem, it seems, boils down to the following:
Package Example is
Task Type Message_Task is
Entry Execute;
End Message_Task;
End Example;
Package Body Example is
Task Body Message_Task is
Use Ada.Text_IO;
Begin
accept Execute do
Put_Line( "Rendezvous!" );
end Execute;
delay 0.2; -- Stub delay.
Put_Line( "Finishing Task." );
-- Task Ends Here.
End Message_Task;
End Example;
--...
Test : Example.Message_Task;
--...
Test.Execute;
-- Test.Execute can't be accepted here because it can only accept "Execute"
-- the one time, as per the body's definition.
The reason that this really is like your problem is because, likewise once you say "X.Start(1,2)" another call to Start doesn't reset the position of the task's execution back up to that accept.
If you wanted the task to "stay alive" for further processing you could do one of two options.
Option 1 -- set up a 'protocol':
Package Example is
Task Type Message_Task is
Entry Initialization;
Entry Execute;
Entry Quit;
End Message_Task;
End Example;
Package Body Example is
Task Body Message_Task is
Use Ada.Text_IO;
Has_quit : Boolean := False;
Begin
Main:
loop
select
accept Initialization do
null;
end Initialization;
accept Execute do
null;
end Execute;
or
accept Quit do
Has_Quit := True;
end Quit;
end select;
Exit Main when Has_Quit;
end loop Main;
End Message_Task;
End Example;
Option 2 -- Allow termination.
Package Example is
Task Type Message_Task is
Entry Initialization;
Entry Execute;
End Message_Task;
End Example;
Package Body Example is
Task Body Message_Task is
Use Ada.Text_IO;
Has_quit : Boolean := False;
Begin
accept Initialization do
null;
end Initialization;
Main:
loop
select
accept Execute do
null;
end Execute;
or
terminate;
end select;
end loop Main;
End Message_Task;
End Example;
The subtle difference is Option 2 gets rid of the Quit entry, allowing the task to 'rest' on the terminate alternative while Option 1 is more explicit in control (and required in some cases), but requiring that Initialization & Execute be called in pairs.
A task only runs until it reaches the end of its main sequence of statements (ignoring various technicalities).
If you want a task to do something, and then pause until it receives an external trigger, you should put a loop around the statements you have in the task body.

Is there a way to test other assertions inside a test after calling tSQLt.ExpectException?

Using tSQLt 1.0.5873.27393, I'm trying to write a tSQLt test for a stored procedure that records errors trapped in a CATCH block to a log table before re-throwing the error to the calling session.
I can successfully test that the error message is re-thrown using tSQLt.ExpectException, but a side effect seems to be that it's not possible to test any other assertions after calling tSQLt.ExpectException - so I am unable to test whether the error has been written to the log table.
The simplest test that I can come up with which demonstrates the problem is:
CREATE PROC MyTests.[test ExpectException]
AS
BEGIN
EXEC tSQLt.ExpectException;
THROW 50001, 'Error message',1
EXEC tSQLt.AssertEquals 0,1, '0=1'
END
GO
which produces the following (unexpected) output when executed:
|No|Test Case Name |Dur(ms)|Result |
+--+--------------------------------+-------+-------+
|1 |[MyTests].[test ExpectException]| 3|Success|
Using a profiler trace, I can see that the AssertEquals assertion is never executed, because the error is trapped by a CATCH block inside tSQLt.Private_RunTest.
Because tSQLt uses CATCH to trap errors, I suspect that there's no solution to this problem without a major rewrite of tSQLt - but I ask here just in case others have found a way around the issue.
You could follow this approach:
In the test, wrap your call to the tested stored procedure in a try/catch block.
Before the try/catch block, set up an expected variable to 1 and actual to 0.
In the test catch block, check if the log table got populated and if yes then flip the actual variable to 1.
Write your assertion AFTER the catch block.
CREATE PROC MyTests.[test ExpectException]
AS
BEGIN
DECLARE #expected bit = 1;
DECLARE #actual bit = 0;
BEGIN TRY
-- call tested SP, make sure it fails
CALL SP..;
-- add safety net, if the SP call doesn't fail then fail the test
RAISERROR('Fail the test', 16, 1);
END TRY
BEGIN CATCH
-- pseudo code
IF EXISTS (SELECT 1 FROM log table)
-> flip #actual to 1
END CATCH
EXEC tSQLt.AssertEquals #expected, #actual
END
GO
#Eduard Uta got there before I did, but I'll post this anyway.
It dawned on me almost as soon as I'd posted this that the obvious solution is to use a custom TRY...CATCH block, rather than the built-in tSQLt objects:
CREATE PROC MyTests.[test custom try catch]
AS
BEGIN
DECLARE #ErrorRaised bit = 0
BEGIN TRY
THROW 50001, 'Error message',1
END TRY
BEGIN CATCH
SET #ErrorRaised = 1
END CATCH
EXEC tSQLt.AssertEquals 1,#ErrorRaised, 'Error raised'
EXEC tSQLt.AssertEquals 0,1, '0=1'
END
GO
Here's an example which tests for a specific error message:
CREATE PROC MyTests.[test custom try catch test message]
AS
BEGIN
DECLARE #ErrorRaised bit = 0
BEGIN TRY
THROW 50001, 'Error message',1
END TRY
BEGIN CATCH
IF ERROR_MESSAGE() = 'Error message'
SET #ErrorRaised = 1
ELSE
THROW
END CATCH
EXEC tSQLt.AssertEquals 1,#ErrorRaised, 'Error raised'
EXEC tSQLt.AssertEquals 0,1, '0=1'
END
GO
For me, calling both the procedure under test and the tSQLt ExpectException proc inside a TRY...CATCH block seems to have done the trick.
CREATE PROC MyTests.[test ExpectException]
AS
BEGIN
BEGIN TRY
-- Call the proc that raises the error
EXEC offending_stored_proc
-- Tell tSQLt to expect the exception
EXEC tSQLt.ExpectException #ExpectedMessage = 'Expected error message', #ExpectedSeverity = NULL, #ExpectedState = NULL;
END TRY
BEGIN CATCH
-- No need to do anything here
END CATCH
END;
As expected, if the procedure raises the error with that error message, the test passes. If the error is not raised, the test fails.

ORA-01841 :(full) year must be between -4713 and +9999, and not be 0 when binding to dynamic sql with DBMS_SQL

When I run (snippets here):
t_cmd := 'begin :1 := other_package.some_function(:0,:1,:2); end;';
l_tmstmp timestamp_unconstrained := to_timestamp('1999-07-07 07:07:07.000000000',
'YYYY-MM-DD HH24:MI:SS.FF9');
DBMS_SQL.PARSE(curid, t_cmd, DBMS_SQL.NATIVE);
-- binding here other attributes
DBMS_SQL.BIND_VARIABLE(curid, ':2', l_tmstmp);
ret := DBMS_SQL.EXECUTE(curid);
I always recieve error, even when try to bind simple timestamp, timestamp from varchar2 (TO_TIMESTAMP('timestamp_in_proper_format'), 'YYYY-MM-DD HH24:MI:SS.FF9')) :
ORA-01841 :(full) year must be between -4713 and +9999, and not be 0
ORA-06512: przy linia 1
ORA-06512: przy "SYS.DBMS_SQL", line 1825
ORA-06512: przy "MY_PACKAGE", line where is :
ret := DBMS_SQL.EXECUTE(curid);
What is interesting problem don't occurs when I debug package.
I tried compile my package :
ALTER PACKAGE my_package COMPILE DEBUG PACKAGE;
but problem still occurs, besides in producation environment it will be compiled with optimization level so I can't do this in that way.
Additionaly I can not use EXECUTE IMMEDIATE because function name, types and number of parameters are known at runtime.
Any idea?
Thank you #OldProgrammer!
When I tried to run simple snippet as you suggested I realized something.
Problem was in this line:
t_cmd := 'begin :1 := other_package.some_function(:0,:1,:2); end;';
I didn't change binding name from this fragment: "begin :1" and now I have two bindings to the same value.
In execute immediate it was ok because there ORDER of bindings is significant, dbms_sql uses names of bindings.
Argument :1 from some_function(:0,:1,:2) is also timestamp.
some_function returns varchar2 so errors occured when it was trying to substitute result to first ":1".
I changed function call to:
t_cmd := 'begin :my_result := other_package.some_function(:0,:1,:2); end;';
And "ORA-01841 :(full) year must be between -4713 and +9999" is gone.
I did not focus on this fragment "begin :1 :=" because tried to get result from function like that:
ret := DBMS_SQL.EXECUTE(curid);
src_cur := DBMS_SQL.TO_REFCURSOR(curid);
FETCH src_cur INTO my_result;
CLOSE src_cur;
but this way getting result from function didn't work (some other errors occured)
Finally I changed above code to:
ret := DBMS_SQL.EXECUTE(curid);
DBMS_SQL.VARIABLE_VALUE(curid, 'my_result', my_result);
DBMS_SQL.CLOSE_CURSOR(curid);
And now everything runs fine.

Resources