go to post Timothy Leavitt · May 12, 2016 Eduard, that actually does run faster for me than the $ListFromString/$ListNext approach (with the conversion included). Not including the conversion, $ListNext is still faster. (I've updated the Gist to include that example.)
go to post Timothy Leavitt · May 12, 2016 Thank you for pointing out both of those things!I'm not sure why I thought the end condition was evaluated each time. I'll update the post to avoid spreading misinformation.
go to post Timothy Leavitt · May 10, 2016 For what it's worth, I believe $Namespace is new'd before CreateProjection/RemoveProjection are called. At least, I was playing with this yesterday and there weren't any unexpected side effects from not including: new $Namespace in those methods. But it definitely is best practice to do so (in general). One effect of this I noticed yesterday is that if you call $System.OBJ.Compile* for classes in a different namespace in CreateProjection, they're queued to compile in the original namespace rather than the current one. Kind of weird, but perhaps reasonable; you can always JOB the compilation in the different namespace. Maybe there's some other workaround I couldn't find.
go to post Timothy Leavitt · May 9, 2016 This is a great article! One minor detail - MyPackage.Installer (or some other class) needs to declare the projection for the installer class to work as advertised. For example, in MyPackage.Installer itself, you could add: Projection InstallMe As MyPackage.Installer; The examples you referenced on GitHub include this.
go to post Timothy Leavitt · May 5, 2016 I don't think this is currently possible in Atelier. (I was looking for the same feature yesterday and couldn't find it.)Using Eclipse as a Java editor, the override menu option is Source -> Override/Implement Methods...; presumably, the equivalent feature in Atelier would be in the same place, but there's nothing like that in the "Source" menu.
go to post Timothy Leavitt · May 4, 2016 There was a similar question and answer at https://community.intersystems.com/post/how-include-dynaform-custom-task-form-ensemble-workflow that might be helpful.In short, the simplest solution (and possibly the only one) would be to put the Zen page in an <iframe> in a CSP page that EnsLib.Workflow.TaskRequest.%FormTemplate points to.
go to post Timothy Leavitt · Apr 30, 2016 I really would recommend creating a %All namespace (if there isn't already one), via %Installer or something that works similarly.One projects on the intersystems-ru github, Caché Web Terminal, has the same requirement (use from any namespace); this class might be helpful for reference: https://github.com/intersystems-ru/webterminal/blob/master/export/WebTerminal/Installer.xml. It doesn't actually use %Installer, so configuration changes are implemented in COS instead of being generated based on XML, but it works similarly.Particularly, see methods CreateAllNamespace and Map/UnMap. You should be able to adapt these without too much effort. If your code coverage project eventually has a UI, then the web application setup method will be useful too (for simple installation).
go to post Timothy Leavitt · Apr 30, 2016 See the documentation on the special %ALL "namespace".%ALL allows for globals/routines/packages to be mapped to all namespaces. Your %Installer (if you're using one) can check to see if this is set up and create it if not, and add the mappings. You might want to map the packages and perhaps system-wide settings your code uses, but perhaps not any namespace-specific data.
go to post Timothy Leavitt · Apr 27, 2016 That's true.Some developers like to use #dim to declare all the variables they expect to create. In Studio, Tools > Options..., Environment > Class, there's an "Option Explicit" option that will give you warnings if you use a variable that hasn't been #dim'd. (The "Track Variables" option is also very useful.)
go to post Timothy Leavitt · Apr 27, 2016 Sure - although it'd be a property, not a parameter. Looking at utcov.ClassLookup (glad to see it's not %utcov now, by the way), this should work fine for you. Here's a sample: Class Sample.ClassQueryProperty Extends %RegisteredObject { Property SubclassQuery As %SQL.Statement [ InitialExpression = {##class(%SQL.Statement).%New()}, Private, ReadOnly ]; Method %OnNew() As %Status [ Private, ServerOnly = 1 ] { Quit ..SubclassQuery.%PrepareClassQuery("%Dictionary.ClassDefinition","SubclassOf") } Method Demo() { Set tRes = ..SubclassQuery.%Execute("%UnitTest.TestCase") While tRes.%Next(.tSC) { $$$ThrowOnError(tSC) Write tRes.%Get("Name"),! } $$$ThrowOnError(tSC) } } Then: SAMPLES>d ##class(Sample.ClassQueryProperty).%New().Demo() %UnitTest.IKnowRegression %UnitTest.PMMLRegression %UnitTest.SQLDataRegression %UnitTest.SQLRegression %UnitTest.SetBuilder %UnitTest.TSQL %UnitTest.TestCacheScript %UnitTest.TestProduction %UnitTest.TestScript %UnitTest.TestSqlScript Wasabi.Logic.Test.InventoryTest Wasabi.Logic.Test.PricingTest
go to post Timothy Leavitt · Apr 25, 2016 In addition to %IsA (or, similarly, %Extends, which considers multiple inheritance rather than just primary superclasses), the following snippet (slightly modified from an answer I posted on one of your previous questions) may be helpful if you're looking for all of the names of unit test classes: Set tStmt = ##class(%SQL.Statement).%New() $$$ThrowOnError(tStmt.%PrepareClassQuery("%Dictionary.ClassDefinition","SubclassOf")) Set tRes = tStmt.%Execute("%UnitTest.TestCase") While tRes.%Next(.tSC) { $$$ThrowOnError(tSC) //TODO: something with tRes.%Get("Name") } $$$ThrowOnError(tSC) If you're filtering by package - and it looks like https://github.com/litesolutions/cache-utcov/blob/master/src/utcov/ClassLookup.cls does this - then you can supply a second argument to the SubclassOf query with the package name for better performance. (i.e., Set tRes = tStmt.%Execute("%UnitTest.TestCase","Some.Package.Name.")) All of these approaches work recursively. (C extends B, B extends A -> C extends A.)
go to post Timothy Leavitt · Apr 22, 2016 The SVG diagram is loaded in Eclipse's internal browser, which will always be IE for you. The preference you found applies to "external" browsers. Within the internal browser in Eclipse, you can right click and select "view source." When you do so, you should see something like this near the top: <meta http-equiv="X-UA-Compatible" content="IE=9" /> It would be interesting to know what <meta> tag you see, if any. It would also be useful to know the value of the User-Agent header sent by the internal browser. There are several ways to find that; here's one quick option: Open a BPL class in AtelierRun the following code in Terminal: k ^%ISCLOG s ^%ISCLOG = 2 read x s ^%ISCLOG = 0 In Atelier, right click in the BPL class and click the "Open diagram editor" popup menu itemHit enter in Terminal to stop logging. If you then zwrite ^%ISCLOG you should see the user-agent in a $listbuild list near the end of the output. I see: ^%ISCLOG("Data",180,0)=$lb(900,,0,5532241409,"0²!t"_$c(28,16)_"IÎ"_$c(22)_"F40"_$c(133)_"¯4_ài"_$c(156)_"èB_9}%"_$c(144,155,9)_"!`"_$c(135)_"ü",2,"ENSDEMO","001000010000OoTvE12bLJWATFMLUAodU0gK1Z8HvjdbJWLK3M",,0,"en-us","OoTvE12bLJ",2,1,"/csp/ensdemo/",$lb("UnknownUser","%All","%All",64,-559038737),"","","","2016-04-22 13:28:27","2016-04-22 13:28:30","","Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.1; Win64; x64; Trident/6.0)","","",0,"",$lb($lb("%ZEN.SessionEvents","ENSDEMO",)),"","%iscmgtportal:5ykW4kOfOzwr7O8gcok8XQ--",0,"","","","","") (It's awesome how IE says it's Mozilla, for compatibility reasons.)
go to post Timothy Leavitt · Apr 21, 2016 You're really close; the key is using the stream's OID (from %Oid()). Here's a simple example; you can substitute any appropriate file path. Class Demo.DynamicImage Extends %ZEN.Component.page { /// This XML block defines the contents of this page. XData Contents [ XMLNamespace = "http://www.intersystems.com/zen" ] { <page xmlns="http://www.intersystems.com/zen" title=""> <image id="myImage" src="" /> <button onclick="zenPage.ChangeImage(zen('myImage'))" caption="Dynamically Change Image" /> </page> } ClassMethod ChangeImage(pImage As %ZEN.Component.image) [ ZenMethod ] { Set tStream = ##class(%Stream.FileBinary).%New() Do tStream.LinkToFile(##class(%File).ManagerDirectory()_"..\CSP\broker\images\einstein.jpg") Set tOID = ..Encrypt(tStream.%Oid()) Set pImage.src = "%25CSP.StreamServer.cls?STREAMOID="_tOID } } I'm really curious what that image is doing in /csp/broker/...
go to post Timothy Leavitt · Apr 21, 2016 I think this was a caution for anyone changing their username, since it's shared across InterSystems' sites/applications.IIRC you use CCR (Change Control Record). The username change may prevent you from using the version control integration in that application. It might be good to ensure that it's still working, or at least to make a note that if it doesn't work, you'll need to change the username back (and then probably log out and back in again for the change to take effect in CCR).Others may not be impacted as much.
go to post Timothy Leavitt · Apr 21, 2016 It's an acronym for "modular object-oriented dynamic learning environment" (https://en.wikipedia.org/wiki/Moodle)Or maybe it's "my oblivious ostrich doesn't like eggplants" [citation needed]
go to post Timothy Leavitt · Apr 20, 2016 Here's some code from the application I'm working on that might help. The "load/delete the test classes" behavior was annoying enough that we decided to always have the classes loaded on development/testing systems.First, I think it's useful to have a Run() method in each unit test class, or in a subclass of %UnitTest.TestCase that your unit tests will extend. This code could live somewhere else too, but it's useful to be able to say: do ##class(my.test.class).Run() and not have to remember/type the test suite format and /nodelete. Sample implementation: Class Tools.UnitTest.TestCase Extends %UnitTest.TestCase { /// Runs the test methods in this unit test class. ClassMethod Run(ByRef pUTManager As %UnitTest.Manager = "", pBreakOnError As %Boolean = 0) { If '$IsObject(pUTManager) { Set pUTManager = ##class(%UnitTest.Manager).%New() //Or Tools.UnitTest.Manager if you have that Set pUTManager.Debug = pBreakOnError Set pUTManager.Display = "log,error" } Set tTestSuite = $Piece($classname(),".",1,*-1) Set qspec = "/noload/nodelete" Set tSC = $$$qualifierParseAlterDefault("UnitTest","/keepsource",.qspec,.qstruct) Do pUTManager.RunOneTestSuite("",$Replace(tTestSuite,".","/"),tTestSuite_":"_$classname(),.qstruct) } } This allows you to specify an instance of a %UnitTest.Manager to capture the test results in, which is useful if you're running a bunch of specific unit test classes (like you suggested, from a Studio project). My team organizes tests in packages rather than in projects, which makes more sense for us. Next up, here's our %UnitTest.Manager subclass that works with the %UnitTest.TestCase subclass shown above, allowing all the classes in a particular namespace or package (or, really, with class names that contain a particular string) to be run without deleting them afterward: Class Tools.UnitTest.Manager Extends %UnitTest.Manager { /// Runs all unit tests (assuming that they're already loaded) /// May filter by package or output to a log file rather than terminal ClassMethod RunAllTests(pPackage As %String = "", pLogFile As %String = "") As %Status { Set tSuccess = 1 Try { Set tLogFileOpen = 0 Set tOldIO = $io If (pLogFile '= "") { Open pLogFile:"WNS":10 Set tLogFileOpen = 1 Use pLogFile } Write "*** Unit tests starting at ",$zdt($h,3)," ***",! Set tBegin = $zh Set tUnitTestManager = ..%New() Set tUnitTestManager.Display = "log,error" Set tStmt = ##class(%SQL.Statement).%New() Set tSC = tStmt.%PrepareClassQuery("%Dictionary.ClassDefinition","SubclassOf") $$$THROWONERROR(tSC,tStmt.%PrepareClassQuery("%Dictionary.ClassDefinition","SubclassOf")) Set tRes = tStmt.%Execute("Tools.UnitTest.TestCase") While tRes.%Next(.tSC) { If $$$ISERR(tSC) $$$ThrowStatus(tSC) Continue:(pPackage'="")&&(tRes.%Get("Name") '[ pPackage) Do $classmethod(tRes.%Get("Name"),"Run",.tUnitTestManager) } If $IsObject(tUnitTestManager) { Do tUnitTestManager.SaveResult($zh-tBegin) Do tUnitTestManager.PrintURL() &sql(select sum(case when c.Status = 0 then 1 else 0 end) as failed, sum(case when c.Status = 1 then 1 else 0 end) as passed, sum(case when c.Status = 2 then 1 else 0 end) as skipped into :tFailed, :tPassed, :tSkipped from %UnitTest_Result.TestSuite s join %UnitTest_Result.TestCase c on s.Id = c.TestSuite where s.TestInstance = :tUnitTestManager.LogIndex) If (tFailed '= 0) { Set tSuccess = 0 } } Else { Write "No unit tests found matching package: ",pPackage,! } } Catch anyException { Set tSuccess = 0 Write anyException.DisplayString(),! } Write !,!,"Test cases: ",tPassed," passed, ",tSkipped," skipped, ",tFailed," failed",! If 'tSuccess { Write !,"ERROR(S) OCCURRED." } Use tOldIO Close:tLogFileOpen pLogFile Quit $Select(tSuccess:1,1:$$$ERROR($$$GeneralError,"One or more errors occurred in unit tests.")) } This could probably be tweaked to use a project instead without too much work, but I think packages are a more reasonable way of organizing unit tests.
go to post Timothy Leavitt · Apr 20, 2016 For more advanced error analysis, such as conversion of error %Status-es into user-friendly messages (as I described in another comment), $System.Status.DecomposeStatus will provide the parameters of the error message as well. These are substituted in to the localizable string. For example, here's a foreign key violation message from %DeleteId on a system running in Spanish: INSYNC>Set tSC = ##class(Icon.DB.CT.TipoDocumento).%DeleteId(50) INSYNC>k tErrorInfo d $System.Status.DecomposeStatus(tSC,.tErrorInfo) zw tErrorInfo tErrorInfo=1 tErrorInfo(1)="ERROR #5831: Error de Foreign Key Constraint (Icon.DB.CC.AllowedGuaranteeTypes) sobre DELETE de objeto en Icon.DB.CT.TipoDocumento: Al menos existe 1 objeto con referencia a la clave CTTIPODOCUMENTOPK" tErrorInfo(1,"caller")="zFKTipoDocDelete+4^Icon.DB.CC.AllowedGuaranteeTypes.1" tErrorInfo(1,"code")=5831 tErrorInfo(1,"dcode")=5831 tErrorInfo(1,"domain")="%ObjectErrors" tErrorInfo(1,"namespace")="INSYNC" tErrorInfo(1,"param")=4 tErrorInfo(1,"param",1)="Icon.DB.CC.AllowedGuaranteeTypes" tErrorInfo(1,"param",2)="Icon.DB.CT.TipoDocumento" tErrorInfo(1,"param",3)="DELETE" tErrorInfo(1,"param",4)="CTTIPODOCUMENTOPK" tErrorInfo(1,"stack")=... The "param" array allows clean programmatic access to the details of the foreign key violation, independent of language. Of course, these level of detail in these error messages may be subject to change across Caché versions, so this is a *great* thing to cover with unit tests if your application relies on it.
go to post Timothy Leavitt · Apr 20, 2016 There are good options for what you want available in 2016.2, and possibly better answers for SQL -> JSON after that. In 2016.2, %RegisteredObject also supports $toJSON and $fromJSON, so there won't be any need to use %ZEN.Auxiliary.jsonProvider to do that conversion. Under the hood, the path is really RegisteredObject -> Dynamic Object (via $compose) -> JSON, and JSON -> Dynamic Object -> RegisteredObject (via $compose) Therefore, the behavior of $toJSON and $fromJSON can be modified for %RegisteredObject subclasses by overriding (typically) %ToDynamicObject and %FromObject. Here's an example that might serve as a useful starting point for Object -> JSON/JSON -> Object on 2016.2+: Class DCDemo.JSONDateTime Extends (%Persistent, %Populate) { Property Name As %String; Property DateField As %Date; Property "Time_Stamp_Field" As %TimeStamp; Property TimeField As %Time; ClassMethod Run() { Do ..%KillExtent() Do ..Populate(1) Set tObj = ..%OpenId(1) Write "Object ID 1",! zw tObj Write ! Set tJSON = tObj.$toJSON() Write "JSON for that object:",! Write tJSON,!,! Set tObj2 = ..$fromJSON(tJSON) Write "Object from that JSON:",! zw tObj2 Write ! } Method %ToDynamicObject(target As %Object = "", ignoreUnknown = 0) [ ServerOnly = 1 ] { Set tObj = ##super(target,ignoreUnknown) Do ..DateTimeToISO8601(tObj) Quit tObj } ClassMethod %FromObject(source = "", target = "", laxMode As %Integer = 1) As %RegisteredObject [ ServerOnly = 1 ] { Set tObj = ##super(source,target,laxMode) If source.%IsA("%Library.AbstractObject") { Do ..ISO8601ToDateTime(tObj) } Quit tObj } ClassMethod DateTimeToISO8601(pObj As %Library.AbstractObject) [ CodeMode = objectgenerator ] { #dim tProp As %Dictionary.CompiledProperty Set tKey = "" For { Set tProp = %compiledclass.Properties.GetNext(.tKey) Quit:tKey="" If (tProp.Type '= "") && 'tProp.ReadOnly && 'tProp.Calculated { Set tType = tProp.Type Set tExpr = "" If $ClassMethod(tType,"%Extends","%Library.Date") { Set tExpr = "Set %arg = $zd(%arg,3)" } ElseIf $ClassMethod(tType,"%Extends","%Library.Time") { Set tExpr = "Set %arg = $zt(%arg,1)" } ElseIf $ClassMethod(tType,"%Extends","%Library.TimeStamp") { Set tExpr = "Set %arg = $Case(%arg,"""":"""",:$Replace(%arg,"" "",""T"")_""Z"")" } Do:tExpr'="" %code.WriteLine($c(9)_$Replace(tExpr,"%arg","pObj."_$$$QN(tProp.Name))) } } } ClassMethod ISO8601ToDateTime(pObj As DCDemo.JSONDateTime) [ CodeMode = objectgenerator ] { #dim tProp As %Dictionary.CompiledProperty Set tKey = "" For { Set tProp = %compiledclass.Properties.GetNext(.tKey) Quit:tKey="" If (tProp.Type '= "") && 'tProp.ReadOnly && 'tProp.Calculated { Set tType = tProp.Type Set tExpr = "" If $ClassMethod(tType,"%Extends","%Library.Date") { Set tExpr = "Set %arg = $zdh(%arg,3)" } ElseIf $ClassMethod(tType,"%Extends","%Library.Time") { Set tExpr = "Set %arg = $zth(%arg,1)" } ElseIf $ClassMethod(tType,"%Extends","%Library.TimeStamp") { Set tExpr = "Set %arg = $Extract($Replace(%arg,""T"","" ""),1,*-1)" } Do:tExpr'="" %code.WriteLine($c(9)_$Replace(tExpr,"%arg","pObj."_$$$QN(tProp.Name))) } } } } The output of this is: USER>d ##class(DCDemo.JSONDateTime).Run() Object ID 1 tObj=<OBJECT REFERENCE>[1@DCDemo.JSONDateTime] +----------------- general information --------------- | oref value: 1 | class name: DCDemo.JSONDateTime | %%OID: $lb("1","DCDemo.JSONDateTime") | reference count: 2 +----------------- attribute values ------------------ | %Concurrency = 1 <Set> | DateField = 40424 | Name = "North,Richard G." | TimeField = 74813 | Time_Stamp_Field = "1963-11-18 01:49:29" +----------------------------------------------------- JSON for that object: {"$CLASSNAME":"DCDemo.JSONDateTime","$REFERENCE":"1","DateField":"1951-09-05","Name":"North,Richard G.","TimeField":"20:46:53","Time_Stamp_Field":"1963-11-18T01:49:29Z"} Object from that JSON: tObj2=<OBJECT REFERENCE>[4@DCDemo.JSONDateTime] +----------------- general information --------------- | oref value: 4 | class name: DCDemo.JSONDateTime | reference count: 2 +----------------- attribute values ------------------ | %Concurrency = 1 <Set> | DateField = 40424 | Name = "North,Richard G." | TimeField = 74813 | Time_Stamp_Field = "1963-11-18 01:49:29" +----------------------------------------------------- The matter of SQL -> JSON is a bit more complicated. ODBC select mode for SQL is similar to ISO 8601, but not completely (the timestamp format is different). One option would be to create a class (extending %RegisteredObject) to represent a query result with date/time fields in ISO 8601 format, and to override the same methods in it so that: It can be $compose'd from a %SQL.IResultSet (done in %FromObject)Based on query column metadata, dates/times/timestamps are converted to the correct format when the object is represented as a %Object/%Array or, indirectly, in JSON (done in %ToDynamicObject / %ToDynamicArray). This could probably be done in 2016.2, but might be less work to accomplish in a future version when SQL result sets support $fromJSON/$toJSON. (I think this plan was mentioned in a different post.) I suppose there are some possible complications with all this, depending on whether times/timestamps in your application are actually local or UTC. (Or worse, a mix...)
go to post Timothy Leavitt · Apr 19, 2016 Happy to help. :-)To clarify, I think it's a difference between ccontrol terminal and ccontrol runw - ccontrol terminal wouldn't accept spaces for me either.
go to post Timothy Leavitt · Apr 19, 2016 Other than locks, there are a few other cases where cleanup may be needed whether or not something goes wrong:Closing SQL cursors that have been openedEnsuring that the right IO device is in use and/or returning to the previous IO redirection state.There are probably more of these too.Here's the convention we use for error handling, logging, and reporting in InSync (a large Caché-based application):We have TSTART/TCOMMIT/TROLLBACK in a try/catch block at the highest level (typically a ClassMethod in a CSP/Zen page). There isn't much business logic in here; it'll call a method in a different package.If anything goes wrong in the business logic, an exception is thrown. The classes with the business logic don't have their own try/catch blocks unless it's needed to close SQL cursors, etc. in event of an exception. After the cleanup is done, the exception is re-thrown. (Unfortunately, this means that cleanup code may be duplicated between the try and catch blocks, but there's typically not too much duplication.) The classes with business logic also don't have their own TSTART/TCOMMIT/TROLLBACK commands, unless the business logic is a batch process in which parts of the process may fail and be corrected later without impacting the whole thing; such a case may also call for a nested try/catch to do the TROLLBACK if something goes wrong in part of the batch. In this case the error is recorded rather than re-throwing the exception.We have our own type of exception (extending %Exception.AbstractException), and macros to create exceptions of this type from:Error %Status codesError SQLCODEs and messagesSQLCODE = 100 can be treated as an error, "alert", or nothing.Other types of exceptionsExceptions of our custom type can also be created to represent a general application error not related to one of those things, either a fatal error, or something the user can/should fix - e.g., invalid data or missing configuration.The macros for throwing these exceptions also allow the developer to provide a localizable user-friendly message to explain what went wrong.When an exception is caught in the top level try/catch (or perhaps in a nested try/catch in a batch process), we have a macro that logs the exception and turns it into a user-friendly error message. This might just be a general message, like "An internal error occurred (log ID _______)" - the user should never see <UNDEFINED>, SQLCODE -124: DETAILS ABOUT SOME TABLE, etc.Our persistent classes may include an XDATA block with localizable error messages corresponding foreign and unique keys in the class and types of violations of those keys. For %Status codes and SQLCODEs corresponding to foreign/unique key violations, the user-friendly error message is determined based on this metadata.Logging for these exceptions is configurable; for example, exceptions representing something the user can/should fix are not logged by default, because they're not an error in the application itself. Also, the log level is configurable - it might be all the gory detail from LOG^%ETN, or just the stack trace. Typically, verbose logging would only be enabled system-wide briefly for specific debugging tasks. For SQL errors, the SQL statement itself is logged if possible.I thought this convention was too complicated when I first started working with it, but have come to see that it is very elegant. One possible downside is that it relies on a convention that any method in a particular package (InSyncCode, in our case) might throw an exception - if that isn't respected in the calling code, there's risk of a <THROW> error.I mentioned the InSync approach previously on https://community.intersystems.com/post/message-error-csppage . Unfortunately, it's coupled with several parts of the application, so it'd be quite a bit of work to extract and publish the generally-applicable parts. I'd like to do that at some point though.