Monday, January 26, 2015

Testing Grunt Plugin From Grunt

Writing tests for grunt plugin turned out to be less straightforward then expected. I needed to run multiple task configurations and wanted to invoke them all by typing grunt test in main directory.

Grunt normally exits after first task failure. That makes it impossible to store multiple failure scenarios inside the main project gruntfile. Running them from there would require the --force option, but grunt then ignores all warnings which is not optimal.

Cleaner solution is to have a bunch of gruntfiles in separate directory and invoke them all from the main project gruntfile. This post explains how to do that.

Table of Contents

Demo Project

Demo project is small grunt plugin with one grunt task. The task either fails with warning or prints success message into the console depending on the value of action options property.

The task:
grunt.registerMultiTask('plugin_tester', 'Demo grunt task.', function() {
  //merge supplied options with default options
  var options = this.options({ action: 'pass', message: 'unknown error'});

  //pass or fail - depending on configured options
  if (options.action==='pass') {
    grunt.log.writeln('Plugin worked correctly passed.');
  } else {
    grunt.warn('Plugin failed: ' + options.message);
There are three different ways how to write grunt plugin unit tests. Each solution has its own nodeunit file in test directory and is explained in this post:
All three demo tests consist of three different task configurations:
// Success scenario
options: { action: 'pass' }
// Fail with "complete failure" message
options: { action: 'fail', message: 'complete failure' }
//Fail with "partial failure" message
options: { action: 'fail', message: 'partial failure' }

Each configuration is stored in separate gruntfile inside test directory. For example, success scenario stored inside gruntfile-pass.js file looks like this:
  // prove that npm plugin works too
  jshint: { 
    all: [ 'gruntfile-pass.js' ] 
  // Configuration to be run (and then tested).
  plugin_tester: { 
    pass: { options: { action: 'pass' } } 

// Load this plugin's task(s).
// next line does not work - grunt requires locally installed plugins

grunt.registerTask('default', ['plugin_tester', 'jshint']);
All three test gruntfiles look almost the same, only the options object of plugin_tester target changes.

Running Gruntfile From Subdirectory

Our test gruntfiles are stored in test subdirectory and grunt does not handle such situation well. This chapter explains what the problem is and shows two ways how to solve it.

The Problem
To see the problem, go to the demo project directory and run following command:
grunt --gruntfile test/gruntfile-problem.js
Grunt responds with following error:
Local Npm module "grunt-contrib-jshint" not found. Is it installed?
Warning: Task "jshint" not found. Use --force to continue.

Aborted due to warnings.
Grunt assumes that grunfile and node_modules repository are stored in the same directory. While node.js require function searches all parent directories for required module, grunts loadNpmTasks does not.

This problem has two possible solutions, simple and fancy one:
  • create local npm repository in tests directory (simple),
  • make grunt load tasks from parent directories (fancy).

Although the first "simple" solution is somewhat cleaner, demo project uses second "fancy" solution.

Solution 1: Duplicate Npm Repository
The main idea is simple, just create another local npm repository inside the tests directory:
  • Copy package.json file into tests directory.
  • Add test only dependencies into it.
  • Run npm install command every time you run tests.

This is the cleaner solution. It has only two downsides:
  • test dependencies have to be maintained separately,
  • all plugin dependencies have to be installed in two places.

Solution 2: Load Grunt Tasks From Parent Directory
The other solution is to force grunt to load tasks from npm repository stored inside another directory.

Grunt Plugin Loading
Grunt has two methods able to load plugins:
  • loadTasks('directory-name') - loads all tasks inside a directory,
  • loadNpmTasks('plugin-name') - loads all tasks defined by a plugin.

The loadNpmTasks function assumes fixed directory structure of both grunt plugin and modules repository. It guess name of directory where tasks should be stored and then calls loadTasks('directory-name') function.

Local npm repository has separate subdirectory for each npm package. All grunt plugins are supposed to have tasks subdirectory and .js files inside it are assumed to contain tasks. For example, loadNpmTasks('grunt-contrib-jshint') call loads tasks from node_mudules/grunt-contrib-jshint/tasks directory and is equivalent to:
Therefore, if we want to load all tasks of grunt-contrib-jshint plugin from parent directory, we can do following:
Loop Parent Directories
More flexible solution is to climb through all parent directories until we find closest node_modules repository or reach root directory. This is implemented inside grunt-hacks.js module.

The loadParentNpmTasks function loops parent directories :
module.exports = new function() {

  this.loadParentNpmTasks = function(grunt, pluginName) {
    var oldDirectory='', climb='', directory, content;

    // search for the right directory
    directory = climb+'node_modules/'+ pluginName;
    while (continueClimbing(grunt, oldDirectory, directory)) {
      climb += '../';
      oldDirectory = directory;
      directory = climb+'node_modules/'+ pluginName;

    // load tasks or return an error
    if (grunt.file.exists(directory)) {
    } else {'Tasks plugin ' + pluginName + ' was not found.');

  function continueClimbing(grunt, oldDirectory, directory) {
    return !grunt.file.exists(directory) &&
      !grunt.file.arePathsEquivalent(oldDirectory, directory);

Modified Gruntfile
Finally, we need to replace the usual grunt.loadNpmTasks('grunt-contrib-jshint') call in the gruntfile by following:
var loader = require("./grunt-hacks.js");
loader.loadParentNpmTasks(grunt, 'grunt-contrib-jshint');

Shortened gruntfile:
module.exports = function(grunt) {
  var loader = require("./grunt-hacks.js");

    jshint: { /* ... */  },
    plugin_tester: { /* ... */ }

  loader.loadParentNpmTasks(grunt, 'grunt-contrib-jshint');

This solution has two disadvantages:
  • It does not deal with collection plugins.
  • If grunt ever changes expected structure of grunt plugins, you will have to modify the solution.

If you need collection plugins too, have a look at grunts task.js to see how to support them.

Calling Gruntfile From Javascript

Second thing we need to do is to invoke the gruntfile from javascript. The only complication is that grunt exits whole process on task failure. Therefore, we need to call it from child process.

Node module child process has three different functions able to run command inside child process:
  • exec - executes command on command line,
  • spawn - differently executes command on command line,
  • fork - runs node module in child process.

The first one, exec, is easiest to use and is explained in the first subchapter. Second subchapter shows how to use fork and why it is less optimal then exec. Third subchapter is about spawn.

Exec runs command line command inside a child process. You can specify in which directory to run it, set up environment variables, set timeout after which the command will be killed and so on. When the command finishes its run, exec calls callback and passes it stdout stream, stderr streams and error if the command crashed.

Unless configured otherwise, command is run in current directory. We want it to run inside tests subdirectory, so we have to specify cwd property of options object: {cwd: 'tests/'}.

Both stdout and stderr streams content are stored inside a buffer. Each buffer has maximum size set to 204800 and if the command produces more output, exec call will crash. That amount is enough for our small task. If you need more you have to set maxBuffer options property.

Call Exec
Following code snippet shows how to run the gruntfile from exec. The function is asynchronous and calls whenDoneCallback after all is done:
var cp = require("child_process");

function callGruntfile(filename, whenDoneCallback) {
  var command, options;
  command = "grunt --gruntfile "+filename+" --no-color";
  options = {cwd: 'test/'};
  cp.exec(command, options, whenDoneCallback);
Note: if you installed npm into tests directory (simple solution), then you need to use callNpmInstallAndGruntfile function instead of callGruntfile:
function callNpmInstallAndGruntfile(filename, whenDoneCallback) {
  var command, options;
  command = "npm install";
  options = {cwd: 'test/'};
  cp.exec(command, {}, function(error, stdout, stderr) {
    callGruntfile(filename, whenDoneCallback);
Unit Tests
First node unit test runs success scenario and then checks whether process finished without failure, whether standard output contains expected message and whether standard error is empty.

Success scenario unit test:
pass: function(test) {
  callGruntfile('gruntfile-pass.js', function (error, stdout, stderr) {
    test.equal(error, null, "Command should not fail.");
    test.equal(stderr, '', "Standard error stream should be empty.");

    var stdoutOk = contains(stdout, 'Plugin worked correctly.');
    test.ok(stdoutOk, "Missing stdout message.");
Second node unit test runs "complete failure" scenario and then checks whether process failed as expected. Note that standard error stream is empty and warnings are printed into standard output.

Failing scenario unit test:
fail_1: function(test) {
  var gFile = 'gruntfile-fail-complete.js';
  callGruntfile(gFile, function (error, stdout, stderr) {
    test.equal(error, null, "Command should have failed.");
    test.equal(error.message, 'Command failed: ', "Wrong error message.");
    test.equal(stderr, '', "Non empty stderr.");

    var stdoutOk = containsWarning(stdout, 'complete failure');
    test.ok(stdoutOk, "Missing stdout message.");
Third "partial failure" node unit test is almost the same as the previous one. Whole tests file is available on github.

  • Maximum buffer size must be set in advance.

Fork runs node.js module inside child process and is equivalent to calling node <module-name> on command line. Fork uses callbacks to send standard output and standard error to caller. Both callbacks can be called many times and caller obtains child process outputs in pieces.

Using fork makes sense only if you need to handle arbitrary sized stdout and stderr or if you need to customize grunt functionality. If you do not, exec is easier to use.

This chapter is split into four sub-chapters:
Call Grunt
Grunt was not meant to be called programatically. It does not expose "public" API and does not document it.

Our solution mimics what grunt-cli does, so it is relatively future safe. Grunt-cli is distributed separately from grunt core and therefore is less likely to change. However, if it does change, this solution will have to change too.

Running grunt from javascript requires us to:
  • separate gruntfile name from its path,
  • change active directory,
  • call grunts tasks function.

Call grunt from javascript:
this.runGruntfile = function(filename) {
  var grunt = require('grunt'), path = require('path'), directory, filename;
  // split filename into directory and file
  directory = path.dirname(filename);
  filename = path.basename(filename);

  //change directory

  //call grunt
  grunt.tasks(['default'], {gruntfile:filename, color:false}, function() {
Module Arguments
The module will be called from command line. Node keeps command line arguments inside process.argv array:
module.exports = new function() {
  var filename, directory;

  this.runGruntfile = function(filename) {
    /* ... */

  //get first command line argument
  filename = process.argv[2];

Call Fork
Fork has three arguments: path to module, array with command line arguments and options object. Call module.js with tests/Gruntfile-1.js parameter:
child = cp.fork('./module.js', ['tests/Gruntfile-1.js'], {silent: true})
The silent: true option makes stdout and stderr of the returned child process available inside the parent. If it is set to true, returned object provides access to stdout and stderr streams of the caller.

Call on('data', callback) on each stream. Passed callback will be called each time the child process sends something to the stream:
child.stdout.on('data', function (data) {
  console.log('stdout: ' + data); // handle piece of stdout
child.stderr.on('data', function (data) {
  console.log('stderr: ' + data); // handle piece of stderr

Child process can either crash or end its work correctly:
child.on('error', function(error){
  // handle child crash
  console.log('error: ' + error); 
child.on('exit', function (code, signal) {
  // this is called after child process ended
  console.log('child process exited with code ' + code); 

Demo project uses following function to calls fork and to bind callbacks:
 * callbacks: onProcessError(error), onProcessExit(code, signal), onStdout(data), onStderr(data)
function callGruntfile(filename, callbacks) {
  var comArg, options, child;
  callbacks = callbacks || {};

  child = cp.fork('./test/call-grunt.js', [filename], {silent: true});

  if (callbacks.onProcessError) {
    child.on("error", callbacks.onProcessError);
  if (callbacks.onProcessExit) {
    child.on("exit", callbacks.onProcessExit);
  if (callbacks.onStdout) {
    child.stdout.on('data', callbacks.onStdout);
  if (callbacks.onStderr) {
    child.stderr.on('data', callbacks.onStderr);

Write Tests
Each unit test calls the callGruntfile function. Callbacks search for expected content inside the standard output stream, check whether exit code was correct, fail when something shows up on error stream or fail if fork call returns an error.

Success scenario unit test:
pass: function(test) {
  var wasPassMessage = false, callbacks;
  callbacks = {
    onProcessError: function(error) {
      test.ok(false, "Unexpected error: " + error);
    onProcessExit: function(code, signal) {
      test.equal(code, 0, "Exit code should have been 0");
      test.ok(wasPassMessage, "Pass message was never sent ");
    onStdout: function(data) {
      if (contains(data, 'Plugin worked correctly.')) {
        wasPassMessage = true;
    onStderr: function(data) {
      test.ok(false, "Stderr should have been empty: " + data);
  callGruntfile('test/gruntfile-pass.js', callbacks);
Tests corresponding to failure scenario are pretty much the same and can be found on github.

  • Used grunt function does not belong to official API.
  • Child process output streams are available in chunks instead of one big block.

Spawn is a cross between fork and exec. Similarly to exec, spawn is able to run an executable file and pass it command line arguments. Child process output streams are treated the same way as in fork. They are send to parent in pieces via callbacks. Therefore, exactly as with fork, using spawn makes sense only if you need arbitrary sized stdout or stderr.

The Problem
The main problem with spawn happens on windows. The name of command to be run must be specified exactly. If you call spawn with an argument grunt, spawn expects executable filename without suffix. Real grunt executable grunt.cmd will not be found. Otherwise said, spawn ignores windows environment variable PATHEXT.

Looping Suffixes
If you want to call grunt from spawn, you will need to do one of the following things:
  • use different code for windows and for linux or
  • read PATHEXT from environment and loop through it until you find the right suffix.

Following function loops through PATHEXT and passes the right filename to the callback:
function findGruntFilename(callback) {
  var command = "grunt", options, extensionsStr, extensions, i, child, onErrorFnc, hasRightExtension = false;

  onErrorFnc = function(data) {
    if (data.message!=="spawn ENOENT"){
      grunt.warn("Unexpected error on spawn " +extensions[i]+ " error: " + data);

  function tryExtension(extension) {
    var child = cp.spawn(command + extension, ['--version']);
    child.on("error", onErrorFnc);
    child.on("exit", function(code, signal) {
      hasRightExtension = true;
      callback(command + extension);

  extensionsStr = process.env.PATHEXT || '';
  extensions = [''].concat(extensionsStr.split(';'));
  for (i=0; !hasRightExtension && i<extensions.length;i++) {
Write Tests
Once you have grunt command name, you are ready to call spawn. Spawn fires exactly the same events as fork, so callGruntfile accepts exactly the same callbacks object and binds its properties to child process events:
function callGruntfile(command, filename, callbacks) {
  var comArg, options, child;
  callbacks = callbacks || {};

  comArg = ["--gruntfile", filename, "--no-color"];
  options = {cwd: 'test/'};
  child = cp.spawn(command, comArg, options);
  if (callbacks.onProcessError) {
    child.on("error", callbacks.onProcessError);
  /* ... callbacks binding exactly as in fork ...*/
Tests are also almost the same as those in the previous chapter. Only difference is that you have to find the grunt executable filename before doing everything else. Success scenario test looks like this:
pass: function(test) {
  var wasPassMessage = false;
    var callbacks = {
      /* ... callbacks look exactly the same way as in fork ... */
    callGruntfile(gruntCommand, 'gruntfile-pass.js', callbacks);
Full success scenario test along with both failure scenarios tests are available on github.

  • Spawn ignores PATHEXT suffixes, custom code to handle it is needed.
  • Child process output streams are available in chunks instead of one big block.


There are three ways how to test grunt plugin from inside gruntfile. Unless you have very strong reason not to, use exec.


genga g said...

Your good knowledge and kindness in playing with all the pieces were very useful. I don’t know what I would have done if I had not encountered such a step like this.
Block Chain Training in chennai

Block Chain Training in annanagar

Block Chain Training in pune

Block Chain Training in velachery

Hare Ram said...

It’s great to come across a blog every once in a while that isn’t the same out of date rehashed material. Fantastic read.

Digital Marketing Training in Mumbai

Six Sigma Training in Dubai

Six Sigma Abu Dhabi



amala jst said...
This comment has been removed by the author.
amala jst said...

Wow it is really wonderful and awesome thus it is very much useful for me to understand many concepts and helped me a lot. it is really explainable very well and i got more information from your blog.

rpa training in chennai | best rpa training in chennai | rpa training in chennai | rpa training in bangalore
rpa training in pune | rpa online training

Anexas Europe said...

Very nice post here and thanks for it .I always like and such a super contents of these post.Excellent and very cool idea and great content of different kinds of the valuable information's.
Good discussion. Thank you.
Six Sigma Training in Abu Dhabi
Six Sigma Training in Dammam
Six Sigma Training in Riyadh

Naga Manickam said...

Good Post! Thank you so much for sharing this pretty post, it was so good to read and useful to improve my knowledge as updated one, keep blogging.
Data Science training in kalyan nagar | Data Science training in OMR
Data Science training in chennai | Data science training in velachery
Data science training in tambaram | Data science training in jaya nagar

afiah ahamed said...

I appreciate your efforts because it conveys the message of what you are trying to say. It's a great skill to make even the person who doesn't know about the subject could able to understand the subject . Your blogs are understandable and also elaborately described. I hope to read more and more interesting articles from your blog. All the best.
java training in chennai | java training in bangalore

java online training | java training in pune

sai said...

Good Post, I am a big believer in posting comments on sites to let the blog writers know that they ve added something advantageous to the world wide web.
online Python certification course
python training in OMR
python training course in chennai

jeeva said...

This is most informative and also this post most user friendly and super navigation to all posts... Thank you so much for giving this information to me.. 

best rpa training in chennai | rpa online training |
rpa training in chennai |
rpa training in bangalore
rpa training in pune

johnsy sai said...

This is most informative and also this post most user friendly and super navigation to all posts... Thank you so much for giving this information to me.. 
Best Devops Training in pune

cynthia williams said...

Thanks for sharing this pretty post, it was good and helpful. Share more like this.
Blockchain Training in Chennai
Blockchain course in Chennai
Angularjs Training in Chennai
AWS Training in Chennai
DevOps Training in Chennai
Python Training in Chennai

Anbarasan14 said...

I believe that your blog will surely help the readers who are really in need of this vital piece of information. Waiting for your updates.

French Class in Mulund
French Coaching in Mulund
French Classes in Mulund East
French Language Classes in Mulund
French Training in Mulund
French Coaching Classes in Mulund
French Classes in Mulund West

Sadhana Rathore said...

Useful content, I have bookmarked this page for my future reference.
Appium Training in Chennai
Best Appium Training institute in Chennai
Appium Certification in Chennai
Mobile Appium Training in Chennai
Mobile Appium course in Chennai

Aruna Ram said...

Very creativity blog!!! I learned a lot of new things from your post. It is really a good work and your post is the knowledgeable. Waiting for your more updates...
Blue Prism Training Institute in Bangalore
Blue Prism Course in Bangalore
Blue Prism Training Bangalore
Blue Prism Classes in Bangalore
Blue Prism Course in Adyar
Blue Prism Training in Mogappair

LindaJasmine said...

Amazing Post . Thanks for sharing. Your style of writing is very unique. Pls keep on updating.
Spoken English Classes in Chennai
Best Spoken English Classes in Chennai
Spoken English Class in Chennai
Spoken English in Chennai
Best Spoken English Class in Chennai
English Coaching Classes in Chennai

sathyaramesh said...
This comment has been removed by the author.
Vicky Ram said...

Thanks for your contribution in sharing such a useful information. Waiting for your further updates.


punitha said...

Nice article I was really impressed by seeing this blog, it was very interesting and it is very useful for me.
Javascript Training in Bangalore
Java script Training in Bangalore
Javascript Training Institutes in Bangalore
Advanced Java Training Institute in Bangalore
Best Institute For Java Course in Bangalore

ijazz jazz said...

Whoa! I’m enjoying the template/theme of this website. It’s simple, yet effective. A lot of times it’s very hard to get that “perfect balance” between superb usability and visual appeal. I must say you’ve done a very good job with this.

AWS Training in Bangalore | Amazon Web Services Training in bangalore , india

AWS Training in pune | Amazon Web Services Training in Pune, india

AWS Training in Chennai|Amazon Web Services Training in Chennai,India

aws online training and certification | amazon web services online training ,india

Gautam krish said...

The blog is well written and Thanks for your information.
JAVA Training Coimbatore
JAVA Coaching Centers in Coimbatore
Best JAVA Training Institute in Coimbatore
JAVA Certification Course in Coimbatore
JAVA Training Institute in Coimbatore

pavithra dass said...

I am obliged to you for sharing this piece of information here and updating us with your resourceful guidance. Hope this might benefit many learners. Keep sharing this gainful articles and continue updating us.
Angularjs Training in Chennai
Angularjs Training
Angularjs Training near me
DevOps Training in Chennai
DevOps certification Chennai
DevOps certification

Aruna Ram said...

Really it was an awesome article!!! It was so good to read and used to improve my knowledge as updated one, keep blogging.....
Data Science Course in Mogappair
Data Science Training in Annanagar
Data Science Training in Ambattur
Data Science Course in Tnagar
Data Science Training in Saidapet
Data Science Course in Vadapalani

Riya Raj said...

Wonderful blog!!! Thanks for your information… Waiting for your upcoming data.
Ethical Hacking Course in Coimbatore
Hacking Course in Coimbatore
Ethical Hacking Training in Coimbatore
Ethical Hacking Training Institute in Coimbatore
Ethical Hacking Training
Ethical Hacking Course

mercyroy said...

Great!it is really nice blog information.after a long time i have grow through such kind of ideas.thanks for share your thoughts with us.
Selenium Courses in T nagar
Selenium Training Institutes in T nagar
Selenium Certification Training in OMR
Selenium Training in Perungudi

Kayal m said...

Great blog!!! It was very impressed to me. I like so much and keep sharing. Thank you.
Robotics Courses in Bangalore
Automation Courses in Bangalore
RPA Courses in Bangalore
Robotics Classes in Bangalore
Robotics Training in Bangalore
Automation Training in Bangalore

Aruna Ram said...

Well post, very useful content and I really impressed. I need more info to your blog. Keep Posting.
SEO Course in Nungambakkam
SEO Training in Saidapet
SEO Course in Tnagar
SEO Course in Omr
SEO Training in Sholinganallur
SEO Course in Navalur

sathyaramesh said...

Nice article. I liked very much. All the informations given by you are really helpful for my research. keep on posting your views.
Salesforce Administrator 201 Training in Chennai
Salesforce Administrator 211 Training in Chennai
Salesforce Developer 401 Training in Chennai
Cloud computing Training in Chennai
Cloud computing Training
Cloud computing Training near me

Gautam krish said...

The blog which you have shared is more informative. Thanks for your information.
JAVA Training Center in Coimbatore
JAVA Training
JAVA Certification Course
JAVA Certification Training
JAVA Training Courses

Riya Raj said...

Outstanding information!!! Thanks for sharing your blog with us.
Spoken English Class in Coimbatore
Best Spoken English Classes in Coimbatore
Spoken English in Coimbatore
Spoken English Classes
English Speaking Course

Riya Raj said...

The information you have shared is more useful to us. Thanks for your blog.
List of Franchise Business in India
Franchise Opportunities in India with Low Investment
Best Franchise Business in India
Frenchies in India
Top Franchise in

kavinilavu G said...

Thank you for sharing your article. Great efforts put it to find the list of articles which is very useful to know, Definitely will share the same to other forums.

best openstack training in chennai | openstack course fees in chennai | openstack certification in chennai | redhat openstack training in chennai

Kayal m said...

Well done! This is really powerful post and also very interesting. Thanks for your sharing and I want more updates from your blog.
Digital Marketing Training in Aminjikarai
Digital Marketing Training in vadapalani
Digital Marketing Course in Chennai
Digital Marketing Training in Omr
Digital Marketing Training in Kelambakkam
Digital Marketing Training in Karappakkam

sathyaramesh said...

I am really enjoying reading your well-written articles. It looks like you spend a lot of effort and time on your blog. I have bookmarked it and I am looking forward to reading new articles. Keep up the good work.
SEO Training in Chennai
Digital Marketing Chennai
Digital Marketing Courses in Chennai
Digital marketing courses
SEO Institutes in Chennai
SEO Course Chennai

Rithi Rawat said...

Outstanding blog thanks for sharing such wonderful blog with us ,after long time came across such knowlegeble blog. keep sharing such informative blog with us.

machine learning classroom training in chennai
machine learning certification in chennai
top institutes for machine learning in chennai
Android training in velachery
PMP training in chennai

DJ PRASATH said...

Thanks for your post. This is excellent information. The list of your blogs is very helpful for those who want to learn, It is amazing!!! You have been helping many application.
best selenium training in chennai | best selenium training institute in chennai selenium training in chennai | best selenium training in chennai | selenium training in Velachery | selenium training in chennai omr | quora selenium training in chennai | selenium testing course fees | java and selenium training in chennai | best selenium training institute in chennai | best selenium training center in chennai

kavinilavu G said...

Such a Great Article!! I learned something new from your blog. Amazing stuff. I would like to follow your blog frequently. Keep Rocking!!
Blue Prism training in chennai | Best Blue Prism Training Institute in Chennai

Robotic Process Automation Tutorial said...

Thank you so much for your information,its very useful and helpful to me.Keep updating and sharing. Thank you.
RPA training in chennai | UiPath training in chennai | rpa course in chennai | Best UiPath Training in chennai

Roja Priya said...

Hi, Thanks a lot for your explanation which is really nice. I have read all your posts here. It is amazing!!!
Keeps the users interest in the website, and keep on sharing more, To know more about our service:
Please free to call us @ +91 9884412301 / 9600112302

Openstack course training in Chennai | best Openstack course in Chennai | best Openstack certification training in Chennai | Openstack certification course in Chennai | openstack training in chennai omr | openstack training in chennai velachery | openstack training in Chennai | openstack course fees in Chennai | openstack certification training in Chennai | best openstack training in Chennai | openstack certification in Chennai

jefrin adams said...

Thanks for the post very impressive

Best Blockchain training in chennai

Clipping Path said...

Thank you so much for the detailed article

lekha mathan said...

Such an excellent and interesting blog, do post like this more with more information, this was very useful, Thank you.
Aviation Courses in Chennai
cabin crew training chennai
airline academy in chennai
airport ground staff training courses in chennai
medical coding course in chennai
fashion designing institute in chennai
best interior design courses in chennai

amsa leka said...

Thanks for your great and helpful presentation I like your good service. I always appreciate your post. That is very interesting I love reading and I am always searching for informative information like this.iot training institutes in chennai | industrial iot training chennai | iot course fees in chennai | iot certification courses in chennai

Earth Fare said...

I couldn’t resist commenting. Exceptionally well written!

Boots Opticians
CiCi's Pizza
Panda Express Feedback from these surveys you can win coupons, Free Fresh Frozen Custard from culvers & Free Donuts within minutes. give your feedback and grab these offers

venu bharath said...

You are an amazing writer. The content is extra-ordinary. Looking for such a masterpiece. Thanks for sharing.
IoT Training in Chennai
IoT courses in Chennai
IoT Courses
IoT Training in Porur
IoT Training in Adyar

Manisha singh said...

Thanks for sharing this valuable information and we collected some information from this blog.
Javascript Training in Delhi

shivani said...

A bewildering web journal I visit this blog, it's unfathomably heavenly. Oddly, in this present blog's substance made the purpose of actuality and reasonable. The substance of data is informative
Oracle Fusion Financials Online Training
Oracle Fusion HCM Online Training
Oracle Fusion SCM Online Training

Joe said...

Tremendous effort. Mind-blowing, Extra-ordinary Post. Your post is highly inspirational. Waiting for your future posts.
Data Analytics Courses in Chennai
Big Data Analytics Courses in Chennai
Data Analytics Courses
Big Data Analytics Training
Big Data Analytics Courses
Data Analytics Certification
Data Analytics Courses in OMR
Data Analytics Courses in Tambaram

Post a Comment