Greetings. Did you consult at InterSystems fifteen years ago? Long time no see.

My favorite technique to keep myself and others honest is to test with random inputs. This gives rise to two challenges: generating the input, and verifying the output. The input obviously depends on the problem: string, number, list, etc. When it comes to output, I look for invariants: x*y=(y*x), sort(x)=sort(shuffle(x)), etc. I sometimes even write another version of the code under test to act as an oracle that's perhaps slower, or not as general.

Caché has at least three ways to generate random numbers: $random(), $system.Encryption.GenCryptRand(), and the Basic Rnd() function.

$random(n) returns a number from 0 to n-1, where n'>1E17.

GenCryptRand(n) returns n bytes of cryptographically random data. You can convert it to a number using one of the $ascii() functions:

  • $a($system.Encryption.GenCryptRand(1))
  • $zwa($system.Encryption.GenCryptRand(2))
  • $zla($system.Encryption.GenCryptRand(4))
  • $zqa($system.Encryption.GenCryptRand(8)) - may be negative

Rnd() is interesting for a tester, because you can seed it with Randomize. If you don't use a seeded PRNG, you'll want to log your inputs somehow. It's frustrating to find a one in a billion bug, but not be able to reproduce it.

Christian in QD came up with a bit of a hack for this. You can use one of the DSNs that is installed automatically with the product, but override all of its attributes; e.g.:

s dsn=dsn_";Database="_$namespace_";Port="_^%SYS("SSPort")_";UID="_usn_";PWD="_pwd

To find an existing DSN, use the SQLDataSources query in the %GTWCatalog class.

He used this technique to make the TestODBC() method in the %UnitTest.SQLRegression class more reliable. I don't know for sure that it will work with a different DBMS and driver, but it's worth a shot.

I dug up a pre-dynamic objects version of a utility method from a REST test and cleaned it up a bit (hopefully not introducing any bugs in the process):

ClassMethod compareArrays(ByRef actual, ByRef expected) As %Status [ PublicList = (actual, expected) ]
{
    ; compare root node
    set deix=$d(expected,eval),daix=$d(actual,aval)
    if deix'=daix {
        quit $$$ERROR($$$GeneralError,"$d(actual)="_daix_" instead of "_deix)
    }
    if deix#2,aval'=eval {
        quit $$$ERROR($$$GeneralError,"actual="""_aval_""" instead of """_eval_"""")
    }

    set status=$$$OK
    set eix="expected",aix="actual"
    for i=1:1 {
        set eix=$q(@eix),aix=$q(@aix)
        quit:""=eix&&(""=aix)

        set seix="("_$p(eix,"(",2,*),saix="("_$p(aix,"(",2,*)
        if seix'=saix {
            set status=$$$ERROR($$$GeneralError,"found """_aix_""" instead of """_eix_""" at position "_i)
            quit
        }

        set deix=$d(@eix,eval),daix=$d(@aix,aval)
        if deix'=daix {
            set status=$$$ERROR($$$GeneralError,"$d(aix)="_daix_" instead of "_deix_" at position "_i)
            quit
        }

        if deix#2,aval'=eval {
            set status=$$$ERROR($$$GeneralError,"actual("""_aix_"""))="""_aval_""" instead of """_eval_""" at position "_i)
            quit
        }
    }
    quit status
}

Comparing them, I only see two things I prefer in my version. First, in this line of your method I would use four-argument $piece with * as the fourth argument, just in case the subscript contains "first" or "second":

    If ($Piece(tRef1,"first",2) '= $Piece(tRef2,"second",2)) {

Second, I would use a public list with first and second, rather than turning off procedure block for the entire method.

You can use the information from %SYS.LockQuery to graph the locks with their owners and waiters. Then do a depth-first traversal of each node, looking for a cycle.

Here's a sketch of building the graph:

s rs=##class(%ResultSet).%New("%SYS.LockQuery:Detail")
s status=rs.Execute()
k graph
f i=1:1 q:'rs.%Next()  d
. s ref="L"_i,graph(ref,rs.Owner)=1
. f j=1:1:$l(rs.WaiterPID," ") d
. . s pid=$p(rs.WaiterPID," ",j) s:pid]"" graph(pid,ref)=1

The graph looks something like this:

graph(3330,"L5")=1
graph(4380,"L4")=1
graph("L1",3309)=1
graph("L2",3326)=1
graph("L3",3327)=1
graph("L4",3330)=1
graph("L5",4380)=1

I've generated IDs for the locks to avoid a SUBSCRIPT error for long references. You'll want to keep a list of the original lock names.

Here's a (minimally tested) traversal method that returns an error if it finds a cycle:

ClassMethod dfs(byref graph, node as %String, byref visited) as %Status {
    s status=$$$OK
    i $d(node) d
    . i $d(visited(node)) d  q
    . . s status=$$$ERROR($$$GeneralError,"found a cycle at node "_node)
    . s visited(node)=1
    . s next=""
    . f  s next=$o(graph(node,next)) q:""=next  d  q:$$$ISERR(status)
    . . s status=..dfs(.graph,next,.visited)
    e  d
    . s root=""
    . f  s root=$o(graph(root)) q:""=root  d  q:$$$ISERR(status)
    . . k visited
    . . s status=..dfs(.graph,root,.visited)
    q status
}

If you try it on the previous graph, it will return an error like the following:

USER>s status=##class(deadlock).dfs(.graph) 

USER>d $system.OBJ.DisplayError(status)    

ERROR #5001: found a cycle at node L5

If you want to wait for either a signal or for termination of a process, you can use a lock:

  1. Process A takes out an exclusive lock.
  2. Process B attempts to lock the same name.
  3. Process A either releases its lock or terminates.
  4. Process B will then get its lock.

You may need an extra synchronization step between steps 1 and 2 to ensure that A gets the lock before B does.

In steps 2 and 4, multiples processes can wait for a shared lock on the same name, and they will all be triggered at the same time.

A quick test suggests that data migration is indeed required after changing the type of a property from %GlobalCharacterStream to %Stream.GlobalCharacter.

  1. Populate a class with some instances, each containing stream data.
  2. The char_length() function in SQL returns the length of the stream field.
  3. Change the type of the property and populate the class with some more instances.
  4. The char_length() function returns the length for the new instances, but null for the old ones.

I imagine the supported way to migrate is to make a new stream field and copy from the old field using CopyFrom(). I'd be tempted to diddle the stream references to point to the existing data. In any case, if it ain't broke ...

You're asking two things: how to persist an object, and how to implement a singleton.

A global on its own is not able to save an object. Something needs to map the structure of its class to a global layout of some kind. The simplest way to do this is to subclass %Persistent, rather than %RegisteredObject, then call %Save().

I notice, however, that you're using %ZEN.proxyObject, presumably to avoid defining a class/schema upfront. In that case, you may be interested in looking at the document data model (DocDM) in the 2016.2 field test.

As for implementing a singleton, it depends on the context. In general, I would look at overriding %OnNew() to load an existing object if it exists. If you want to persist its state, you'll need to consider concurrency.

Different people are going to have different workflows for unit testing, but I would encourage you to settle on a way to get those tests out of the database. I'd be nervous about running tests in a namespace that contains my only copy, since %UnitTest is designed to load and delete by default.

If you're using Studio with the source control hooks, it works pretty well to write the tests in a development namespace, then run them in a test namespace.