Emesary

From FlightGear wiki
Jump to navigation Jump to search
This article is a stub. You can help the wiki by expanding it.
Emesary ported to Node.js (JavaScript


Richard Harrison has finished porting his Emesary system to Nasal and it's now part of the base package. Emesary is a simple and efficient class-based inter-object communication system to allow decoupled disparate parts of a system to function together without knowing about each. It allows decoupling and removal of dependencies by using notifications to cause actions or to query values.

There are many possible applications for Emesary within the system. It solves the problem of how to connect generic instruments to aircraft that have different implementations and properties that need to be used. Richard has been looking at how to abstract out the interface to the MFD and NavDisplay.

At the highest level Emesary is a way of communicating information between objects that are otherwise unrelated and possibly separated by a physical (machine, network, process) boundary. Two (or more) systems can be connected together by using a bridge; the bridge that Richard implemented uses the generic string properties to transmit the messages transparently (to both the sender and recipient). (documentation is possibly slightly out of date for the most recent developments that are in Richard's fgdata) Richard has gotten the transmission of properties working using a new notification type.[1]

There is a set of notes at [1] that explains the whole thing with a worked example (ACLS).

This is in Richard's git repository in his commit, together with the changes to the AI carriers to add ACLS (carrier ILS): https://sourceforge.net/u/r-harrison/fgdata/ci/54165c213f03638a4cb02c848c4f2b234c537f66/

Richard has plans to extend this to be able to transmit over mp (probably layered on top of mp-broadcast). It would also probably work quite well with the HLA FOM to give us a way for models to communicate with other models.[2]

  1. Richard Harrison  (Jun 14th, 2017).  [Flightgear-devel] Emesary / Multiplayer improvements .
  2. Richard Harrison (Apr 1st, 2016). [Flightgear-devel] Message passing interface for Nasal.

Status

The longer term direction Richard wants to take is to extend the multiplayer protocol to better handle Emesary notifications bridged over MP; it has significant advantages which he's covered previously.[1]

Meanwhile, Richard has nearly finished his improvements to Emesary to make it easy and seamless to transmit properties over MP instead of the of the usual way. This is a two part set of changes, the first part (complete) is to transmit from the master model, the second part is still WIP and relates to the transmission back for dual control.


  var PropertySyncNotification =
  {
     new: func(_ident="none", _name="", _kind=0, _secondary_kind=0)
     {
         var new_class = PropertySyncNotificationBase.new(_ident, _name, _kind, _secondary_kind);

         new_class.addIntProperty("consumables/fuel/total-fuel-lbs", 1);
         new_class.addIntProperty("controls/fuel/dump-valve", 1);
         new_class.addIntProperty("engines/engine[0]/augmentation-burner", 1);
         new_class.addIntProperty("engines/engine[0]/n1", 1);
         new_class.addIntProperty("engines/engine[0]/n2", 1);
         new_class.addNormProperty("surface-positions/wing-pos-norm", 2);
#... etc ...
         return new_class;
     }
};

#
# this section sets up the notifications that will be routed and the transmitters that are used
# I've separated out the transmitter that will be used to send the properties to enable better control.
var routedNotifications = [notifications.PropertySyncNotification.new(nil), notifications.GeoEventNotification.new(nil)];
var bridgedTransmitter = emesary.Transmitter.new("outgoingBridge");
var outgoingBridge = emesary_mp_bridge.OutgoingMPBridge.new("F-14mp",routedNotifications, 19, "", bridgedTransmitter);
var incomingBridge = emesary_mp_bridge.IncomingMPBridge.startMPBridge(routedNotifications);
var f14_aircraft_notification = notifications.PropertySyncNotification.new("F-14"~getprop("/sim/multiplay/callsign"));

That's all that is required to ship properties between multiplayer modules via Emesary. There is a limit of 128 bytes on a string which limits the amount of outgoing messages. Outgoing notifications are queued and transmitted as space permits. The way the bridge works is to publish the notification, encoded into an MP string, for a period of time to allow for lagging clients and network issues. If a notification IsDistinct then the bridge will transfer just the last message received; otherwise the bridge will transfer all received notifications over MP. In this sense IsDistinct indicates that the contents of the notification are accurate and definitive (e.g. surface position), so the last value is the most accurate. Other notification (e.g. button 12 pushed will always need to be transferred). Obviously using this technique a variable number of properties can be transmitted, and importantly it's up to the modeller to decide what to transmit. There is a flag that I've temporarily added (sim/multiplay/transmit-only-generics) that doesn't transmit the standard properties, just chat and the generics (to make more space in the packet). There can be different types of notifications sent at a different schedule (so you could have a 10 second update of very slow moving items).[2]

Work in progress

What Richard is working on now is how to make (possibly multiple) slaves communicate back to a master craft, initially the F-14 backseat. The design goal is that the same code should work for both the master and the slave, and that there should not need to be any extra logic. At the highest level this would be (master craft): 1. Inside the model bindings a notification is raised, or a property changed that will cause a notification to be raised. 2. The model receives the notification and the appropriate property is changed. 3. The property changed will be sent over Emesary to all slaves. Slaves will not transmit this data back to the master as there is no outgoing bridge configured to do this. For the slave aircraft 1. Inside the model bindings a notification is raised, or a property changed that will cause a notification to be raised. 2. The notification is bridged over MP and received by the master craft.[3]

ALS landing lights visible on the runway from outside views are with FG since 2016.3 or so - same with the technology of lights external to the aircraft specified via relative geometry (used e.g. to illuminate the Shuttle from the ground floodlights during night landings or SRB separation at night). The latter can also be used for scenery to player or MP to MP illumination, and Richard is toying with the idea of using the Emesary MP bridge for the job.[4] Any lighting always requires some form communication between light source and effect assigned to the surface. In Rembrandt, lots of performance is burned to set this up fairly generally via additional passes and buffers. The ALS technique requires you to set this up specifically - so there has to be an information exchange between light and effect - which is what the light manager does for the Shuttle, and which is what Emesary can do more generally between scenery object Nasal snippets and an aircraft.[5]

There are certain things that work better as addons, and with the work Richard has been doing on Emesary FlightGear is in a better position to have integration of addons than it was previously - simply because Emesary removes inter-dependencies.[6]


Most of the work that Richard did to the MP protocol - extending the number of properties available and improving the efficiency of transfer came out of the need for the OPRF aircraft to transmit more properties - but is something that has much wider applications. Equally Emesary developments are coming along nicely - we should soon (next few months) see Emesary used on the OPRF fleet to allow models to communicate with each other in a more structured way to provide chaff, flares, radar, missiles, bombs, link16, damage etc. [7]

To work over MP properly the model needs to use the existing property transfer over MP (e.g. in sim/multiplay/generic/). The best and possibly easiest way to do this is using Emesary with a multiplayer bridge to transmit this (transparently) using a GeoEventNotification. [8]

Background

We all agree having a good IPC mechanism for distributing the simulation state across threads/processes/network is critical[9]

Properties are much too fine-grained for exposing via HLA. Properties, and subsystems communicating using them, are implementation details of particular HLA federate, not something we’d expose across the federation.[10]

Most solutions that don’t involve fine-grained locking of each property ultimately equate to message passing anyway[11]

Besides, there's the ownship assumption hard-coded all over the place - for example, right now subsystems like the replay (flight recorder) system only record and replay your "own" aircraft flight dynamics.

It would be a significant change/addition and somewhat non-trivial to capture all the data for all the AI objects and replay them as well.

We'd have to decide if the recording and playback mechanism should be part of the AI object or something external that grabs the data and then somehow can force the AI object back through it's original path during playback. [12]

A similar problem arises when you have a multi viewer environment where you want to interact with the carrier or when you watch AI traffic across the views ..

In this case each viewer has its own carrier AI traffic. That means the can move independently. The same applies to multiplayer mode.

We need some generic code which is able to track SGSubsystems including their child subsystems and transfer their relevant states over the network. The same interface could be used to store replay replay logs. [13]

Terminology

Transmitter

Notification

Recipient

Examples

Loading the framework

The Emesary framework is now part of the base package and available automatically. There is nothing special to be done to load it.

Setting up a transmitter

Note  Usually, you will just want to use the existing globalTransmitter
var myTransmitter =  emesary.Transmitter.new("myTransmitter");

Setting up a notification

var HelloNotification =
{
    new: func(message)
    {
        var new_class = emesary.Notification.new("Hello", message);
        return new_class;
    },
};

Setting up a receiver

var HelloRecipient =
{
    new: func(_ident)
    {
        var new_class = emesary.Recipient.new(_ident);
        new_class.count = 0;
        new_class.Receive = func(notification)
        {
            if (notification.Type == "Hello")
            {
                me.count = me.count + 1;
                print("Hello message received:", notification.message);
                return emesary.Transmitter.ReceiptStatus_OK;
            }
            return emesary.Transmitter.ReceiptStatus_NotProcessed;
        };
        return new_class;
    },
};

Registering a receiver

var receiver = HelloRecipient.new("my test receiver");
myTransmitter.Register(receiver);

Notification

myTransmitter.NotifyAll( HelloNotification.new("world !") );

Multiplayer bridge

Richard has the Emesary multiplayer bridge working ref: http://chateau-logic.com/content/emesary-multiplayer-bridge-flightgear [1]

Code is in the branch of fgdata: https://sourceforge.net/u/r-harrison/fgdata/ci/next-emesary-mp-bridge/tree/

  • Nasal/emesary.nas
  • Nasal/emesary_mp_bridge.nas
  • Nasal/notifications.nas

The multiplayer bridge allows notifications to be routed over MP. The model creates an incoming bridge specifying the notifications that are to be received and the bridge will messages from multiplayer models. The elegance of the bridge is that neither the sender nor the receiver need to know about each other; all notifications just appear in the recipient method where they can be handled. Each aircraft would have one (or more recipients) and just handle the incoming message.

Create an incoming and outgoing in a model with the following lines.

var routedNotifications = [notifications.GeoEventNotification.new(nil)];
var outgoingBridge = emesary_mp_bridge.OutgoingMPBridge.new("F-15mp",routedNotifications);
var incomingBridge = emesary_mp_bridge.IncomingMPBridge.startMPBridge(routedNotifications);

then any GeoEventNotification will arrive via MP and the transmitter in any registered recipients, ready for handling like this

var EmesaryRecipient =
{
     new: func(_ident)
     {
         var new_class = emesary.Recipient.new(_ident);

         new_class.Receive = func(notification)
         {
             if (notification.NotificationType == "GeoEventNotification")
             {
                 print("received GeoNotification from ",notification.Callsign);
                 print (" pos=",notification.Position.lat(),notification.Position.lon(),notification.Position.alt());
                 print ("  kind=",notification.Kind, " skind=",notification.SecondaryKind);
                 if(notification.FromIncomingBridge)
                 {
                     if(notification.Kind == 1)# created
                     {
                         if(notification.SecondaryKind >= 48 and notification.SecondaryKind <= 63)
                         {
                             # TBD: animate drop tanks
                         }
                     }
                 }
                 return emesary.Transmitter.ReceiptStatus_OK;
             }
             return emesary.Transmitter.ReceiptStatus_NotProcessed;
         };
         new_class.Response = ANSPN46ActiveResponseNotification.new("ARA-63");
         return new_class;
     },
};
#
#
# Instantiate receiver.
var recipient = EmesaryRecipient.new("F-15-recipient");
emesary.GlobalTransmitter.Register(recipient);
  1. Richard Harrison (Apr 25th, 2016). Re: [Flightgear-devel] Message passing interface for Nasal.

C++ Port

The C++ version should be compatible with the Nasal port, i.e. so that the same Emesary notifications, transmitter and recipients can be used in scripting space and native code, it would make sense to look at the Nasal/CppBind framework to accomplish that.

For the time being, $SG_SRC/canvas/layout/NasalWidget.cxx demonstrates how a C++ "interface" (base class) can be overridden from scripting space by using a corresponding wrapper (see $FG_ROOT/Nasal/canvas/gui/Widget.nas).

Use Cases

With Richard's Emesary system now being ported to Nasal, it would actually make sense to look at it with a focus on FGPythonSys, i.e. how this could be moved to C++ space, and reused by the whole FGScriptingSys interface we've been talking about, because that could make timers, and listeners, entirely unnecessary - while providing the option to run scripts asynchronously from the main loop. It would even be possible to come up with a Canvas mode where a Canvas (FBO) is only updated using an Emesary transmitter, which would mean that these updates could be processed in a background thread, and that OSG could be also much more aggressive about updating Canvas elements/FBOs concurrently. Note that there is nothing Nasal specific about Emesary - it's just a way to come up with a decoupled design that lets components act with eachother without having to know much/anything about their internals, and this kind of decoupling would also be useful for any HLA efforts, because HLA would be just one kind of transport, and one that could greatly benefit from having this separation in place.[1]

Advanced Weather/MP

1rightarrow.png See Advanced_weather#Connection_with_the_multiplayer_system for the main article about this subject.

Richard's Emesary framework would be ideal for this sort of thing. The changes AW side would be modest - basically a shift to a dedicated random number generator, a routine to build a tile based on incoming information, one to store and encode the meta-data of tiles already created and that'd basically be it.[2]


Examples

Collection of examples discussed on the forum.

MP Traffic feeds (injection)

JavaScript port

Richard's Emesary MPI system ported to JavaScript, running in Firefox as part of the Instant-Cquotes script

Torsten is successfully using JavaScript via nodejs for a private project with FlightGear. Communication with FGFS runs via websocket and the http/fgcommand interface, everything works as one would expect; the same technique would be possible with Python or any other scripting language that supports websockets, http-get, http-post and has some support for JSON (python has, ruby has, lua has).[3]

Almost every system should be able to run completely independently of the frame rate. What of course would be mandatory is to sync properties at well defined time stamps, this is what the RTI takes care of. [4]

Instead of adding just-another-feature we need to strip it down to getting a fast and constand fps rendering engine. Everything else needs to run outside the main loop and has to interact with the core by, say HLA/RTI or whatever IPC we have.[5]


WIP.png Work in progress
This article or section will be worked on in the upcoming hours or days.
See history for the latest developments.
 //---------------------------------------------------------------------------
 //
 //	Title                : EMESARY inter-object communication
 //
 //	File Type            : Implementation File
 //
 //	Description          : Provides generic inter-object communication. For an object to receive a message it
 //	                     : must first register with an instance of a Transmitter, and provide a Receive method
 //
 //	                     : To send a message use a Transmitter with an object. That's all there is to it.
 //  
 //  References           : http://www.chateau-logic.com/content/class-based-inter-object-communication
 //                       : http://chateau-logic.com/content/emesary-efficient-inter-object-communication-using-interfaces-and-inheritance
 //                       : http://chateau-logic.com/content/c-wpf-application-plumbing-using-emesary
 //
 //	Author               : Richard Harrison (richard@zaretto.com)
 //
 //	Creation Date        : 29 January 2016
 //
 //	Version              : 4.8
 //
 //  Copyright � 2016 Richard Harrison           Released under GPL V2
 //
 //---------------------------------------------------------------------------*/

function print () {
 var i, msg="";
 for(i=0; i<arguments.length; i++) 
   msg += arguments[i];
 console.log(msg);
}

function inherit(parent) {
    return Object.create(parent);
}

var emesary = (function() {

var Transmitter =
{
    ReceiptStatus_OK : 0,          // Processing completed successfully
    ReceiptStatus_Fail : 1,        // Processing resulted in at least one failure
    ReceiptStatus_Abort : 2,       // Fatal error, stop processing any further recipieints of this message. Implicitly failed.
    ReceiptStatus_Finished : 3,    // Definitive completion - do not send message to any further recipieints
    ReceiptStatus_NotProcessed : 4,// Return value when method doesn't process a message.
    ReceiptStatus_Pending : 5,     // Message sent with indeterminate return status as processing underway
    ReceiptStatus_PendingFinished : 6,// Message definitively handled, status indeterminate. The message will not be sent any further

    new: function(_ident)
    {
        var new_class = inherit(Transmitter);
        new_class.Recipients = [];
        new_class.Ident = _ident;
        return new_class;
    },

    // Add a recipient to receive notifications from this transmitter
    Register: function (recipient)
    {
        this.Recipients.push(recipient);
    },

    // Stops a recipient from receiving notifications from this transmitter.
    DeRegister: function(todelete_recipient)
    {
        var out_idx = 0;
        var element_deleted = 0;

        for (var idx = 0; idx < this.RecipientCount(); idx += 1)
        {
            if (this.Recipients[idx] != todelete_recipient)
            {
                this.Recipients[out_idx] = this.Recipients[idx];
                out_idx = out_idx + 1;
            }
            else
                element_deleted = 1;
        }

        if (element_deleted)
            this.Recipients.pop();
    },

    RecipientCount: function ()
    {
        return this.Recipients.length;
    },

    PrintRecipients: function ()
    {
        print("Recipient list");
        for (var idx = 0; idx < this.RecipientCount(); idx += 1)
            print("Recpient ",idx," ",this.Recipients[idx].Ident);
    },

    // Notify all registered recipients. Stop when receipt status of abort || finished are received.
    // The receipt status from this method will be 
    //  - OK > message handled
    //  - Fail > message not handled. A status of Abort from a recipient will result in our status
    //           being fail as Abort means that the message was not and cannot be handled, and
    //           allows for usages such as access controls.
    NotifyAll: function(message)
    {
        var return_status = Transmitter.ReceiptStatus_NotProcessed;
	var recipient;
	
        this.Recipients.forEach( function(recipient, index, array) 
        {
            if (recipient.Active)
            {
            var rstat = recipient.Receive(message);
            if(rstat == Transmitter.ReceiptStatus_Fail)
            {
                return_status = Transmitter.ReceiptStatus_Fail;
            }
            else if(rstat == Transmitter.ReceiptStatus_Pending)
            {
                return_status = Transmitter.ReceiptStatus_Pending;
            }
            else if(rstat == Transmitter.ReceiptStatus_PendingFinished)
            {
                return rstat;
            }
//            else if(rstat == Transmitter.ReceiptStatus_NotProcessed)
//            {
//                ;
//            }
            else if(rstat == Transmitter.ReceiptStatus_OK)
            {
                if (return_status == Transmitter.ReceiptStatus_NotProcessed)
                    return_status = rstat;
            }
            else if(rstat == Transmitter.ReceiptStatus_Abort)
            {
                return Transmitter.ReceiptStatus_Abort;
            }
            else if(rstat == Transmitter.ReceiptStatus_Finished)
            {
                return Transmitter.ReceiptStatus_OK;
            }
        }
        });
        return return_status;
    },
    // Returns true if a return value from NotifyAll is to be considered a failure.
    IsFailed: function(receiptStatus)
    {
        // Failed is either Fail || Abort.
        // NotProcessed isn't a failure because it hasn't been processed.
        if (receiptStatus == Transmitter.ReceiptStatus_Fail || receiptStatus == Transmitter.ReceiptStatus_Abort)
            return 1;
        return 0;
    }
};

//
//
// Base class for Notifications. By convention a Notification has a type and a value.
//   SubClasses can add extra properties || methods.
var Notification =
{
    new: function(_type, _value)
    {
        var new_class = inherit(Notification);
        new_class.Value = _value;
        new_class.Type = _type;
        return new_class;
    },
};

var Recipient =
{
    new: function(_ident)
    {
        var new_class = inherit(Recipient);
        if (_ident === undefined || _ident === "")
        {
            _ident = id(new_class);
            print("ERROR: Ident required when creating a recipient, defaulting to ",_ident);
        }
        return Recipient.construct(_ident, new_class);
    },
    construct: function(_ident, new_class)
    {
        new_class.Ident = _ident;
        new_class.Active = 1;
        new_class.Receive = function(notification)
    {
        print("ERROR: Receive function not implemented in recipient ",this.Ident);
        return Transmitter.ReceiptStatus_NotProcessed;
        };
        return new_class;
    },
};

return {
	Transmitter: Transmitter,
	Notification: Notification,
	Recipient: Recipient
};

})();

//
// Instantiate a Global Transmitter
var GlobalTransmitter =  emesary.Transmitter.new("GlobalTransmitter");


 //---------------------------------------------------------------------------
 //
 //	Title                : EMESARY tests
 //
 //	File Type            : Implementation File
 //
 //	Author               : Richard Harrison (richard@zaretto.com)
 //
 //	Creation Date        : 29 January 2016
 //
 //  Copyright � 2016 Richard Harrison           Released under GPL V2
 //
 //---------------------------------------------------------------------------*/

print("Emesary tests");

var TestFailCount = 0;
var TestSuccessCount = 0;

var TestNotification =
{
    new: function(_value)
    {
        var new_class = emesary.Notification.new("TestNotification", _value);
        return new_class;
    },
};
var TestNotProcessedNotification =
{
    new: function(_value)
    {
        var new_class = emesary.Notification.new("TestNotProcessedNotification", _value);
        return new_class;
    },
};
var RadarReturnNotification =
{
    new: function(_value, _x, _y, _z)
    {
        var new_class = emesary.Notification.new("RadarReturnNotification", _value);
        new_class.x = _x;
        new_class.y = _y;
        new_class.z = _z;
        return new_class;
    },
};

var TestRecipient =
{
    new: function(_ident)
    {
        var new_class = emesary.Recipient.new(_ident);
	console.log(new_class);
        new_class.count = 0;
        new_class.Receive = function(notification)
        {
            if (notification.Type == "TestNotification")
            {
                this.count = this.count + 1;
                return emesary.Transmitter.ReceiptStatus_OK;
            }
            return emesary.Transmitter.ReceiptStatus_NotProcessed;
        };
        return new_class;
    },
};

var TestRadarRecipient =
{
    new: function(_ident)
    {
        var new_class = emesary.Recipient.new(_ident);
        new_class.Receive = function(notification)
        {
            if (notification.Type == "RadarReturnNotification")
            {
                print(" :: Test recipient ",this.Ident, " recv:",notification.Type," ",notification.Value);
                print(" ::   ",notification.x, " ", notification.y, " ", notification.z);
                return emesary.Transmitter.ReceiptStatus_OK;
            }
            return emesary.Transmitter.ReceiptStatus_NotProcessed;
        };
        return new_class;
    },
};

function PerformTest(tid, t)
{
    if (t())
    {
        TestSuccessCount = TestSuccessCount + 1;
        print("  Test [Pass] :",tid);
    }
    else
    {
        TestFailCount = TestFailCount + 1;
        print("  Test [Fail] :",tid);
    }
}

var tt = TestRecipient.new("tt recipient");
var tt1 = TestRecipient.new("tt1 recipient1");
var tt3 = TestRecipient.new("tt3 recipient3");
var tt2 = TestRadarRecipient.new("tt2: Radar Test recipient2");

PerformTest("Create Notification", 
            function ()
            {
                var tn = TestNotification.new("Test notification"); 
                return tn.Type == "TestNotification" && tn.Value == "Test notification";
            });

PerformTest("Register tt", 
            function ()
            {
                GlobalTransmitter.Register(tt);
                return GlobalTransmitter.RecipientCount() == 1; 
            });
PerformTest("Register tt1", 
            function ()
            {
                GlobalTransmitter.Register(tt1);
                return GlobalTransmitter.RecipientCount() == 2; 
            });
PerformTest("Register tt2", 
            function ()
            {
                GlobalTransmitter.Register(tt2);
                return GlobalTransmitter.RecipientCount() == 3; 
            });
PerformTest("Register tt3", 
            function ()
            {
                GlobalTransmitter.Register(tt3);
                return GlobalTransmitter.RecipientCount() == 4; 
            });

PerformTest("Notify", 
            function ()
            {
                var rv = GlobalTransmitter.NotifyAll(TestNotification.new("Test notification"));
                return !emesary.Transmitter.IsFailed(rv) && rv != emesary.Transmitter.ReceiptStatus_NotProcessed && tt.count == 1; 
            });

PerformTest("DeRegister tt1", 
            function () 
            {
                GlobalTransmitter.DeRegister(tt1);
                return GlobalTransmitter.RecipientCount() == 3; 
            });

tt1_count = tt1.count;
PerformTest("NotifyAfterDeregister", 
            function ()
            {
                GlobalTransmitter.NotifyAll(TestNotification.new("Test notification"));
                return tt1.count == tt1_count;
            });

tt.Active = 0;
tt_count = tt.count;

PerformTest("Recipient.Active", 
            function ()
            {
                var rv = GlobalTransmitter.NotifyAll(TestNotification.new("Test notification"));
                return !emesary.Transmitter.IsFailed(rv) && rv != emesary.Transmitter.ReceiptStatus_NotProcessed && tt.count == tt_count; 
            });


PerformTest("Test Not Processed Notification", 
            function ()
            {
                var rv = GlobalTransmitter.NotifyAll(TestNotProcessedNotification.new("Not Processed"));
                return rv == emesary.Transmitter.ReceiptStatus_NotProcessed; 
            });


GlobalTransmitter.NotifyAll(RadarReturnNotification.new("Radar notification", "x0","y0","z0"));

if (!TestFailCount)
    print("Emesary: All ",TestSuccessCount," tests passed\n");
else
    print("Emesary: ERROR: Tests completed: ",TestFailCount," failed && ",TestSuccessCount," passed\n");

Related


References