1. Suppose $TLevel > (tInitTLevel + 1). That means that someone else's transaction was left open. You can't always guarantee that the code you're calling will behave by matching tstart with tcommit or trollback 1, but you can account for the possibility of your dependency misbehaving in your own transaction cleanup. Agreed on never using argumentless trollback.

2. Great point - updated accordingly.

I think the answers so far have missed the point. The number of arguments itself is variable. This is handy for things like building a complex SQL statement and set of arguments to pass to %SQL.Statement:%Execute, for example.

The data structure here is an integer-subscripted array with the top node set to the number of elements. (The top node is what's missing in the example above). Subscripts can be missing to leave the argument at that position undefined.

Here's a simple example:

Class DC.Demo.VarArgs
{

ClassMethod Driver()
{
    Do ..PrintN(1,2,3)
    
    Write !!
    For i=1:1:4 {
        Set arg($i(arg)) = i
    }
    Kill arg(3)
    ZWrite arg
    Write !
    Do ..PrintN(arg...)
}

ClassMethod PrintN(pArgs...)
{
    For i=1:1:$Get(pArgs) {
        Write $Get(pArgs(i),"<undefined>"),!
    }
}

}

Output is:

d ##class(DC.Demo.VarArgs).Driver()
1
2
3


arg=4
arg(1)=1
arg(2)=2
arg(4)=4

1
2
<undefined>
4

For bootstrap-table, I think the examples on their site are probably more useful than anything I could dig up. https://examples.bootstrap-table.com/#welcomes/large-data.html shows pretty good performance for a large dataset. Tabulator looks nice too though.

In any case it would probably be cleanest to load data via REST rather than rendering everything in the page in an HTML table and then using a library to make the table pretty.

From the pros/cons, it seems the objectives are:

  • Maintain compatibility with normal installation (without ZPM)
  • Make side effects from installation/uninstallation auditable by putting them in module.xml

I'd suggest as one approach to accomplish both objectives:

  • Suppress the projection side effects when running in a package manager installation/uninstallation context (either by checking $STACK or using some trickier under-the-hood things with singletons from the package manager - regardless, be sure to unit test this behavior!).
  • Add "Resource Processor" classes (specified in module.xml with Preload="true" and not included in normal WebTerminal XML exports used for non-ZPM installation) - that is, classes extending %ZPM.PackageManager.Developer.Processor.Abstract and overriding the appropriate methods - to handle your custom installation things. You can then use these in your module manifest, provided that such inversion of control still works without bootstrapping issues following changes made in https://github.com/intersystems-community/zpm.
    • Generally-useful things like creating a %All namespace should probably be pushed back to zpm itself.

This is nifty! Note, you can make the extent manager happy by using:


Class DC.Demo.SometimesPersistent Extends %Persistent
{

Property Foo As %String;

ClassMethod Demo()
{
    New %storage,%fooD,%fooI,%fooS
    Set obj = ##class(DC.Demo.SometimesPersistent).%New()
    Set obj.Foo = "bar"
    Set %storage = "%foo"
    Write !,obj.%Save()
    Kill obj
    Set obj = ..%OpenId(1)
    w ! zw obj
    zw %fooD
}

Storage Default
{
<Data name="SometimesPersistentDefaultData">
<Value name="1">
<Value>%%CLASSNAME</Value>
</Value>
<Value name="2">
<Value>Foo</Value>
</Value>
</Data>
<DataLocation>@($Get(%storage,"^DC.Demo.SometimesPersistent")_"D")</DataLocation>
<DefaultData>SometimesPersistentDefaultData</DefaultData>
<IdLocation>@($Get(%storage,"^DC.Demo.SometimesPersistent")_"D")</IdLocation>
<IndexLocation>@($Get(%storage,"^DC.Demo.SometimesPersistent")_"I")</IndexLocation>
<StreamLocation>@($Get(%storage,"^DC.Demo.SometimesPersistent")_"S")</StreamLocation>
<Type>%Library.CacheStorage</Type>
}

}

Hi Steve,

We (Application Services - internal applications @ InterSystems) use %UnitTest with some extensions. We have a base Unit Test case that has a "run this test" helper method (among other things), a wrapper around %UnitTest.Manager that makes it easier to run all the tests without deleting them (and with some other features optionally enabled, like test coverage measurement - see a video of my 2018 Global Summit presentation on this here). Our wrapper also loads all of the unit tests before running any, which allows unit tests to extend classes within the unit test root in different packages from their own.

I agree with @Eduard Lebedyuk that %UnitTest meets our needs well aside from the lack of parallelization. A parallel %UnitTest runner would be an interesting project indeed...

We routinely run unit tests via Jenkins CI, report test results in the jUnit format, and also report on code coverage (Cobertura-style) and a complexity/coverage scatter plot.

For automated UI testing, Selenium/Cucumber has worked well for older Zen/CSP UIs. True unit testing of newer UIs (e.g,. Jasmine and Karma for Angular) is handy too.

Disclaimer: I know more about what John is doing than is covered in the post.

It looks like, for the prebuilt themes, Angular Material itself uses Bazel (see introduction at https://angular.io/guide/bazel). The relevant bits are here:

https://github.com/angular/components/blob/master/src/material/prebuilt-themes/BUILD.bazel
https://github.com/angular/components/blob/master/src/material/core/BUILD.bazel
https://github.com/angular/components/blob/master/src/material/BUILD.bazel

I think that's probably the place to start, in terms of Angular 8 best practices.

With correct web server configuration to route everything through the CSPGateway, the above example should handle URLs for other resources like that without issue (as long as the content served by the dashboard server has relative links to those endpoints, not absolute) - that's the point of the <base> element.

In my sample use case, it also handles several requests for images and other assets.

I don't think it's totally stupid. We're looking at doing something similar with another technology (JReport).

Here's a code sample to help you get started - just change the port / page / filters / permission checks appropriately.

Class Demo.CSPProxy Extends %CSP.Page
{

Parameter HOST = "127.0.0.1";

Parameter PORT = 8888;

Parameter PAGE = "jinfonet/runReport.jsp";

/// Event handler for <b>PreHTTP</b> event: this is invoked before
/// the HTTP headers for a CSP page have been sent.  All changes to the
/// <class>%CSP.Response</class> class, such as adding cookies, HTTP headers,
/// setting the content type etc. must be made from within the OnPreHTTP() method.
/// Also changes to the state of the CSP application such as changing
/// %session.EndSession or %session.AppTimeout must be made within the OnPreHTTP() method.
/// It is prefered that changes to %session.Preserve are also made in the OnPreHTTP() method
/// as this is more efficient, although it is supported in any section of the page.
/// Return <b>0</b> to prevent <method>OnPage</method> from being called.
ClassMethod OnPreHTTP() As %Boolean [ ServerOnly = 1 ]
{
    Set request = ##class(%Net.HttpRequest).%New()
    Set request.Server = ..#HOST
    Set request.Port = ..#PORT
    
    // TODO: Add other stuff here, like authentication.
    
    Set page = $Piece(%request.CgiEnvs("REQUEST_URI"),"/"_$classname()_".cls/",2)
    If (page = "") {
        Set %base = $classname()_".cls/"_$Piece(..#PAGE,"/",1,*-1)_"/"
        
        // TODO: add query parameters from %request to the URL requested below.
        $$$ThrowOnError(request.Get(..#PAGE))
    } Else {
        Set fullPage = "http://"_..#HOST_":"_..#PORT_"/"_page
        Do ##class(%Net.URLParser).Parse(fullPage,.parts)
        
        // TODO: Better way of checking the requested resource.
        If $Piece($Piece(parts("path"),"/",*),".",2) = "jsp" {
            Set %response.Status = ##class(%CSP.REST).#HTTP403FORBIDDEN
            Quit 0
        }
        $$$ThrowOnError(request.Send(%request.Method,page))
    }
    Set %data = request.HttpResponse.Data
        
    // TODO: Do any other headers matter?
        
    Set %response.Status = request.HttpResponse.StatusCode
    Set %response.ContentType = request.HttpResponse.ContentType
    Quit 1
}

/// Event handler for <b>PAGE</b> event: this is invoked in order to  
/// generate the content of a csp page.
ClassMethod OnPage() As %Status [ ServerOnly = 1 ]
{
    If (%response.ContentType [ "html") && $Data(%base) {
        &html<<base href="#(..EscapeHTML(%base))#">>
        Do %data.OutputToDevice()
    } Else {
        Do %data.OutputToDevice()
    }
    Quit $$$OK
}

}