How It’s Tested¶
In the first part, the JSN library was presented, and also the basic concepts of the Petri Nets and the JSN library were explained. Here, some information about about the internals of the JSN library will be discussed, so you can understand how it works, and, perhaps, develop it further.
I think that the essential skill is testing, so I’ll start with explanation, how the JSN library is tested. Once I’ll explain some tests, I’m going to show you the code.
Let’s start¶
At the moment of writing this, the JSN library wasn’t ready to be published. Well, I’m rather demanding when it comes to code (not that I never write any shitty one!), so I always try to ace it before publish, but I think I have to make an exception here. Just remember, that the code of the JSN library is far from being perfect, and some parts need to be rewritten.
Before we’ll dive into any code, let’s try to clone the JSN repository and build the library:
$ git clone https://bitbucket.org/tprimke/jsn.git
$ cd jsn
$ npm install
$ npm run build
Please note, how easy it is: all you have to do is to clone the repository, go to the directory, and then let the NPM do the rest.
Now, we can find the library in the ‘build’ directory.
For all the tasks like building and testing, gulp is used. But you don’t have to call gulp directly, since all the important commands can be executed as npm scripts.
Now, let’s see, how we can use that built JSN library in other projects.
Let’s create a separate project:
$ cd ..
$ mkdir test-project
$ cd test-project
$ npm init
After the command npm init, you have to answer many questions. Don’t bother with them, you can go on with the default values - it doesn’t matter here.
Right now, we have no access to the JSN library:
$ node
> var jsn = require('jsn');
Error: cannot find module 'jsn'
We can get the access in two different ways:
- install the JSN library from the npm repository,
- use the local version of the library.
If we are going to work on the JSN library - develop new features, perhaps fix bugs - the best solution is to use our own version of the JSN library.
Let’s get back to the JSN library directory and link the library to the local node.js/npm ecosystem:
$ cd ../jsn
$ npm link
Now, we have to get back to our project’s directory and tell npm to use the linked library:
$ cd ../test-project
$ npm link jsn
$ node
> var jsn = require('jsn');
The advantage of such a way of development is that any change made to our local JSN library will be available in our project. And that’s what we need, since we are going to use the JSN library in our projects, and develop the library at the same time.
OK, let’s get back to the JSN library, and try to test it.
$ cd ../jsn
$ npm run test
JSN engine
✓ empty net
✓ token
✓ evaluating arc expression does not affect tokens
✓ firing transition affects token only by the returned value
- when there are no place nor global tokens, arc is not ready
validation
✓ disconnected place
✓ disconnected transition
✓ arc with incomplete data 1
✓ arc with incomplete data 2
✓ arc with incomplete data 3
✓ arc of incorrect place
✓ arc of incorrect transition
✓ more than one arc for a place and a transition
✓ arc evaluate should have ready
✓ arc evaluate should have at least one of: place
✓ arc evaluate should have at least one of: global
✓ arc evaluate should have at least one of: update place
✓ arc evaluate should have at least one of: update global
✓ arc evaluate - time should be positive integer
count up
✓ getPlaces
✓ getPlace
✓ readyTransitions
✓ counting up
✓ reset
Simple nets
✓ countdown
✓ global token
✓ time in place
✓ time in global
- two products of different times
✓ throwing arc
- global token modified after tick
place events
✓ onUpdate on set
✓ onUpdate on update
✓ onUpdate on timed set - undefined
✓ onUpdate on timed set
✓ onUpdate on timed update
transition events
- onReady
✓ onFired
✓ onTick
✓ automatic fire on ready
✓ automatic fire on fired
✓ automatic fire on tick
✓ default events priorities
✓ onReady should be called once - auto
✓ onReady should be called once
✓ onReady can be called many times - auto
✓ onReady can be called many times
automatic transitions
- fired on initialisation
- fired after other transitions
- fired after tick
update token
✓ update place
✓ update global
✓ update with time
✓ update affects place and global only by the returned values
47 passing (58ms)
7 pending
As we can see, most tests are passing, and some of them are pending / skipped. Well, in fact, the test code needs some cleaning. Let’s take a look at the tests.
There are some basic tests (empty net, tokens, evaluating arc expressions). There are also some validation tests, but I’m not going to explain them, since I think all the validation code should be rewritten from scratch. It simply doesn’t work as it should. Then we have tests for simple nets (or models), like the counters. The tests for place and transition events are also very important, since using events is a way to automatically control the net. Please don’t look at the automatic transition tests, since the concept of the automatic transition has been abandoned in favour of place and transition events. And the last group, so far, is update token tests. Those tests check the results of updating token values, instead of producing a brand new ones.
As I have already said, the test code needs some serious cleaning. There are 47 tests passing, and all the pending tests should be removed. As you can see, the number of tests is not so big, and yet the library works very well. Of course, I expect the number of tests to increase, since the library will be developed in order to make your master’s thesis possible to complete.
(By the way, I expect the most changes will be caused by the introduction of the shared places and shared transitions, needed in order to control the simulation using the internet. This “little” change can affect the evaluation of the arc expressions, which are the very heart of the JSN library. What was synchronous before, will become asynchronous, and it can even cause all the code that works today to simply break. So, the very first change made to the library will be the introduction of the shared places and transitions.)
Basic tests¶
Let’s take a look at the package.json file and see, what is done, when we run the command npm run test.
"scripts": {
"test": "./node_modules/.bin/gulp test",
"build": "./node_modules/.bin/gulp build"
},
As we can see, this command simply runs the gulp’s task called “test”. So, let’s take a look at this task, in the gulpfile.
gulp.task( 'test', ['build-es6'], function() {
return gulp.src(['test/test-*.js'], {read: false})
.pipe(mocha({
reporter: 'spec'
}));
});
This task reads all the JavaScript files in the test directory (the files prefixed with the “test-”), and sends them to the mocha test runner. But before it is done, all the es6 files are built.
gulp.task( 'build-es6', ['clean'], function() {
return gulp.src('src/**/*.es6')
.pipe(to5())
.pipe(rename( function(path) {
path.extname = '.js';
}))
.pipe( gulp.dest('build') );
});
The es6 files are JavaScript files written in the ECMAScript 6 standard. As we can see, the task ‘build-es6’ compiles all the files with the es6 extension in the source directory and places the compiled code in the build directory. And just before it’s done, the build directory is cleaned.
So, the JSN library is written in ES6, and the tests are in the ‘test’ directory, and they are standard JS files. Let’s take a look at them.
This is only one file in the directory so far. I think it should be split into separate modules, in order to improve readability. Let’s take a look at this file.
var mocha = require('mocha'),
should = require('should'),
sinon = require('sinon'),
_ = require('lodash'),
jsn = require('../build/jsn');
At the beginning, some modules are imported. I think that mocha is not needed, but the should library is. We also use the sinon library (for mocks), the lodash library, and, of course, the JSN library itself.
After the modules are loaded, the tests begin. Let’s take a look at the first test, for the empty net.
it( 'empty net', function() {
var net = jsn.Net();
// the time should be 0, and it should be possible to tick
net.getTime().should.eql(0);
net.tick();
net.getTime().should.eql(1);
// empty global token
var token = net.getToken();
(token === undefined).should.be.true;
// no transitions, no places
(net.readyTransitions().length).should.eql(0);
(net.getPlaces().length).should.eql(0);
// no errors
(net.getError() === undefined).should.be.true;
// getting a place or firing a transition should throw
net.should.have.property('getPlace');
net.getPlace.should.be.a.Function;
net.should.have.property('fire');
net.fire.should.be.a.Function;
(function() {net.getPlace('x');}).should.throw();
(function() {net.fire('x');}).should.throw();
});
At the beginning, we create a new, empty net. The net is empty (or SHOULD be empty), since no specification for the net is given. Such an empty net is not useful at all, but it was a good start for this first test.
Then, we check the time of our net, and we try to tick the timer, and check the net time once again. The net may be empty, but its internal timer should work even without any places, transitions and arcs.
There are tests for the global token and errors, but don’t bother with them. I think that the global token feature will be removed, since it wasn’t needed while working on the prototype simulation. Right now I think it’s not needed at all, and I see no reason for maintaining this feature.
As for errors, the whole concept of handling errors should be rethought, and after that the code will be probably rewritten from scratch.
So, let’s focus on the tests for transitions and places. We check the number of ready transitions (which should equal to 0, since there are no transitions in our model), we check the number of places. Then, we check the existence of some methods, like getPlace and fire, and we check, whether they throw exceptions. They should throw exceptions, since there are no places that could be got, nor transitions that could be fired.
As for usage of the ‘should’ assertion library, please refer to its homepage.
Well, before we dive into the JSN library code, let’s examine one more test. I think that the tests for the ‘count up’ counter are a good example.
describe( 'count up', function() {
var net;
beforeEach( function() {
net = jsn.Net({
places: [
{ name: 'place', token: 0 }
],
transitions: [
{ name: 'count' }
],
arcs: [
{ place: 'place', transition: 'count',
evaluate: {
ready: true,
place: function(token) { return token + 1; }
} }
]
});
});
The tests are defined in a separate suite. The suite provides the net variable, which is used for storing our counter model.
The beforeEach function is called before each test case is run. The function simply creates our counter model.
And the model is quite simple: it consists of one place (which holds the state of the counter), one transition (used to increase the counter), and one arc to connect them. The initial value of the counter is 0, the arc’s ready expressions is always ‘true’, so the transition is always ready, and the arc’s place expression is simply increasing the counter value.
In this function, the ES5 syntax is used. It will be probably rewritten in ES6.
After the beforeEach function, there are two auxiliary functions: checkPlace and checkReady. The first one examinates the given place object: it checks the name and token properties. The second one checks, whether this is exactly one ready transition in the model, and whether the transition’s name is ‘count’.
var checkPlace = function( place, value ) {
place.should.be.an.Object;
place.should.have.property('name', 'place');
place.should.have.property('token', value);
};
var checkReady = function( net ) {
var ready = net.readyTransitions();
ready.should.be.an.Array;
ready.length.should.eql(1);
ready[0].should.eql('count');
};
Now, let’s take a look at the tests.
it('getPlaces', function() {
var places = net.getPlaces();
places.length.should.eql(1);
checkPlace( places[0], 0 );
// modifying the places shouldn't affect the net
places[0].name = 'alpha';
places[0].token = 2;
places = net.getPlaces();
places.length.should.eql(1);
checkPlace( places[0], 0 );
});
The first test checks the result of the getPlaces function. This function should return the array of all available places in our model. So, we check whether only one place is available, and whether the token value in this place is equal to 0.
These are the basic tests we can perform. Let’s ask an interesting question, though: what should happen, when we CHANGED the returned objects? Well, my idea was that changing the returned objects shouldn’t affect the net model at all. (If we want to affect the tokens in our places, we should fire some transitions instead.) The getPlaces function should merely return a kind of ‘view’ on the real model places.
This behaviour is tested by CHANGING the name and the token values of the returned place object. After such a change, we get the array of places once again, and check, whether the returned place object has the properties set after the net model initialisation. In other words, the fact, that we had changed those values, shouldn’t affect the values returned by the getPlaces function in any way.
The getPlace test is very similar, so we won’t analyse it here.
And here are the transition tests.
it('readyTransitions', function() {
checkReady( net );
});
it('counting up', function() {
net.fire('count');
// the transition should be ready to fire
checkReady( net );
// the counter should be 1
checkPlace( net.getPlace('place'), 1 );
});
it('reset', function() {
net.fire('count');
net.reset();
checkReady( net );
checkPlace( net.getPlace('place'), 0 );
});
The first test simply checks, whether after initialisation the only transition is ready. Nothing interesting, so far.
More interesting is the counting up test. After the count transition is fired, the transition should be ready (since it should be always ready), and the token in the place should has the value of 1.
The last test checks the reset function. First, we fire the transition, and then we reset our model. After the reset, the transition should be ready and the token in the place should has the value of 0, because reset should cause the model to have the same state as just after the initialisation.
Now, it’s time to take a look at the JSN library code.
var _ = require('lodash');
// The main, object-creating function.
var Net = function( conf = {} ) {
return (function() {
...
In the first line, we import the ‘lodash’ library. Well, it’s possible, that in the future I’ll prepare a custom build of this library just for the JSN library, in order to squeeze the JSN size a little.
Then, we have our Net function. This function should return out net model, according to the specification (the conf argument, which is an empty object by default).
In this place, let’s note that:
- The JSN library is written in ES6 - we use the default value for the
confargument. - By default, the
confargument is an empty object. So, calling theNetfunction without any arguments should return an empty net model - just as we supposed in our first test.
OK, let’s get to the function’s body. Here, the module pattern is used: what we return from the function, is the result of anonymous JavaScript function (an object), which acts like a module. The module ENCAPSULATES all its internal state (variables), and in order to find out what it provides, we have to look for the return statement.
return {
getTime: getTime,
tick: tick,
getToken: getToken,
readyTransitions: readyTransitions,
fire: fire,
getPlaces: getPlaces,
getPlace: getPlace,
getError: getError,
reset: reset
};
As we can see, the module provides us some functions. There are functions for time management, for getting information about places and transitions, for firing transitions, and for resetting the model.
There are also other functions, for the global token and for errors handling, but this part of the library will be probably deprecated, so I won’t focus on them.
Now, let’s get back to the beginning of the module.
// the similation time
var time;
// (...)
// the arrays of places, arcs and transitions
// Initially, all of them are simply copies of their specification
// objects.
var places;
var arcs;
var transitions;
// (...)
// objects used to handle net events
// events - the container for all events.
// events.place :: array
// events.transition :: array
// event_id - the next event (unique) identifier to use
// events_handled - the flag set to true, when the events are
// being handled
//
// Probably all the events stuff should me moved to a separate
// module.
var events;
var event_id;
var events_handled;
We can see the module’s private variables declarations. The most important for us are the time, places, transitions and arcs. All the events are also very important, but we won’t focus on them now.
So, let’s get back to the end of our module...
// the initialisation
initNet();
checkNet();
And here we have two function calls. The first one is supposed to initialise our model, and the second one is supposed to validate it. The validation code sucks, so I won’t focus on it here. Let’s check the initialisation code now.
var initNet = () => {
places = getDefault( conf.places, [] );
token = getDefault( conf.token, undefined );
arcs = getDefault( conf.arcs, [] );
transitions = getDefault( conf.transitions, [] );
time = 0;
global_time = [];
events = {
transition: [],
place: []
};
event_id = 0;
events_handled = false;
// set the $awaiting for transitions
let on_ready_trans = _.filter( transitions, (t) => t.hasOwnProperty('onReady') );
_.forEach( on_ready_trans, (t) => t.$awaiting = 'ready' );
//fireAutoTransitions();
checkOnReady();
handleEvents();
};
As we can see, at the beginning, we assign all the module’s variables. Right now, let’s focus on places, transitions and arcs. We assign to them whatever was passed as the configuration (or specification), or empty arrays, when no specification was given.
And that’s all. The remaining code is about handling events, and I won’t discus it here and now.
Right after we have created the initial net, it is ready, and the net time is set to zero.
Let’s check some simple functions, like getPlaces and getPlace. Their names are so descriptive, that they don’t need any further documentation.
var getPlaces = () => {
return _.map( places, (place) => _.clone(place, true) );
};
var getPlace = (name) => {
var place = _.find(places, { name: name });
if ( place !== undefined )
return _.clone( place, true );
else
throw Error('No place ' + name);
};
The getPlaces simply returns the result of mapping the places array over a simple, cloning function. So, the user gets the array of CLONES. And thus, if a clone is modified, the change doesn’t affect the original place object.
The getPlace function is a little more complex. At first, we try to find the place of the given name. And then, when the place is found, its clone is returned.
Now, let’s have a look at the readyTransitions function.
var readyTransitions = () => {
var ready = _.filter( transitions, (t) => isReady(t) );
return _.map( ready, (t) => t.name );
};
First, we filter all the transitions in order to find the ones, which are ready. Then, we return an array of their names, using the map operation.
And what does it mean, that a transition is ready? Let’s have a look at the isReady function.
// For the given transition object, returns true, iff the
// transition is ready.
var isReady = (t) => {
var the_arcs = arcsOfTransition(t.name);
return _.all( the_arcs, (arc) => arcReady(arc) );
};
The implementation should be quite obvious. We take all the arcs connected to our transition, and then we return true, if - and only if - all of them are ready.
How the function arcsOfTransition works?
// Returns the array of all arcs bound with the transition of the
// given name.
var arcsOfTransition = (t_name) => {
return _.filter( arcs, (arc) => arc.transition === t_name );
};
Let’s see: it is simply a filter of all the arcs, and only the arcs connected to our transition are passed. Quite simple.
The arcValue function is responsible for evaluating the ready arc expression. In this function, we get the place object for our arc. Then, when the ready expression is a function, we call this function and pass two arguments: the place token and the global token. When the ready expression is not a function, we simply return the value of this expression.
That’s why we can create ready arc expressions, which are always true: all we need to do is to set them to true.
As you can see, the library is written in a declarative way.