grunt 插件_从Grunt测试Grunt插件
grunt 插件
编写针对grunt插件的测试结果比预期的要简单。 我需要运行多个任务配置,并想通过在主目录中键入grunt test
来调用它们。
在第一个任务失败后,咕声通常会退出。 这使得不可能在主项目gruntfile中存储多个失败方案。 从那里运行它们将需要--force
选项,但是grunt会忽略所有不是最佳的警告。
较干净的解决方案是在单独的目录中有一堆gruntfile,然后从主项目gruntfile调用它们。 这篇文章解释了如何做到这一点。
示范项目
演示项目是带有一个grunt任务的小型grunt插件。 根据action
options属性的值,任务要么失败并显示警告,要么将成功消息打印到控制台中。
任务:
grunt.registerMultiTask('plugin_tester', 'Demo grunt task.', function() {//merge supplied options with default optionsvar options = this.options({ action: 'pass', message: 'unknown error'});//pass or fail - depending on configured optionsif (options.action==='pass') {grunt.log.writeln('Plugin worked correctly passed.');} else {grunt.warn('Plugin failed: ' + options.message);}
});
有三种不同的方式编写grunt插件单元测试。 每个解决方案在test
目录中都有自己的nodeunit文件,并在这篇文章中进行说明:
- plugin_exec_test.js –最实用的解决方案 ,
- plugin_fork_test.js – 解决了先前解决方案失败的罕见情况,
- plugin_spawn_test.js – 可能 ,但最不实用。
所有这三个演示测试都包含三种不同的任务配置:
// 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' }
每个配置都存储在test
目录内的单独gruntfile中。 例如,存储在gruntfile-pass.js
文件中的成功方案如下所示:
grunt.initConfig({// prove that npm plugin works toojshint: { all: [ 'gruntfile-pass.js' ] },// Configuration to be run (and then tested).plugin_tester: { pass: { options: { action: 'pass' } } }
});// Load this plugin's task(s).
grunt.loadTasks('./../tasks');
// next line does not work - grunt requires locally installed plugins
grunt.loadNpmTasks('grunt-contrib-jshint');grunt.registerTask('default', ['plugin_tester', 'jshint']);
这三个测试gruntfiles看起来几乎相同,只有plugin_tester
目标的options
对象改变了。
从子目录运行Gruntfile
我们的测试gruntfiles存储在test
子目录中,而grunt不能很好地处理这种情况。 本章介绍了问题所在,并介绍了两种解决方法。
问题
要查看问题,请转到演示项目目录并运行以下命令:
grunt --gruntfile test/gruntfile-problem.js
Grunt响应以下错误:
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假定grunfile和node_modules存储库存储在同一目录中。 虽然node.js require
函数会在所有父目录中搜索所需模块,但loadNpmTasks
不会。
这个问题有两种可能的解决方案,一种简单而有趣:
- 在测试目录( 简单 )中创建本地npm存储库,
- 从父目录中执行繁重的加载任务( fancy )。
尽管第一个“简单”解决方案比较干净,但演示项目使用了第二个“精美”解决方案。
解决方案1:复制Npm存储库
主要思想很简单,只需在tests目录内创建另一个本地npm存储库:
- 将
package.json
文件复制到tests
目录。 - 向其中添加仅测试依赖项。
- 每次运行测试时,请运行
npm install
命令。
这是更清洁的解决方案。 它只有两个缺点:
- 测试依赖项必须单独维护,
- 所有插件依赖项都必须安装在两个位置。
解决方案2:从父目录加载Grunt任务
另一个解决方案是强制grunt从存储在另一个目录中的npm存储库加载任务。
Grunt插件加载
Grunt有两种方法可以加载插件:
loadTasks('directory-name')
–将所有任务加载到目录中,loadNpmTasks('plugin-name')
–加载插件定义的所有任务。
loadNpmTasks
函数采用grunt插件和模块存储库的固定目录结构。 它猜测应该存储任务的目录名称,然后调用loadTasks('directory-name')
函数。
本地npm存储库为每个npm软件包都有单独的子目录。 所有grunt插件都应该具有tasks
子目录,并且其中的.js
文件都包含任务。 例如, loadNpmTasks('grunt-contrib-jshint')
调用从node_mudules/grunt-contrib-jshint/tasks
目录加载任务,等效于:
grunt.loadTasks('node_modules/grunt-contrib-jshint/tasks')
因此,如果要从父目录加载grunt-contrib-jshint
插件的所有任务,可以执行以下操作:
grunt.loadTasks('../node_modules/grunt-contrib-jshint/tasks')
循环父目录
更为灵活的解决方案是遍历所有父目录,直到找到最近的node_modules存储库或到达根目录为止。 这是在grunt-hacks.js
模块内部实现的。
loadParentNpmTasks
函数循环父目录:
module.exports = new function() {this.loadParentNpmTasks = function(grunt, pluginName) {var oldDirectory='', climb='', directory, content;// search for the right directorydirectory = climb+'node_modules/'+ pluginName;while (continueClimbing(grunt, oldDirectory, directory)) {climb += '../';oldDirectory = directory;directory = climb+'node_modules/'+ pluginName;}// load tasks or return an errorif (grunt.file.exists(directory)) {grunt.loadTasks(directory+'/tasks');} else {grunt.fail.warn('Tasks plugin ' + pluginName + ' was not found.');}}function continueClimbing(grunt, oldDirectory, directory) {return !grunt.file.exists(directory) &&!grunt.file.arePathsEquivalent(oldDirectory, directory);}}();
修改后的Gruntfile
最后,我们需要通过以下步骤替换grunt.loadNpmTasks('grunt-contrib-jshint')
中通常的grunt.loadNpmTasks('grunt-contrib-jshint')
调用:
var loader = require("./grunt-hacks.js");
loader.loadParentNpmTasks(grunt, 'grunt-contrib-jshint');
缩短的gruntfile:
module.exports = function(grunt) {var loader = require("./grunt-hacks.js");grunt.initConfig({jshint: { /* ... */ },plugin_tester: { /* ... */ }});grunt.loadTasks('./../tasks');loader.loadParentNpmTasks(grunt, 'grunt-contrib-jshint');
};
缺点
该解决方案有两个缺点:
- 它不处理集合插件。
- 如果grunt曾经改变grunt插件的预期结构,则必须修改解决方案。
如果您还需要集合插件,请查看grunts task.js以了解如何支持它们。
从Java脚本调用Gruntfile
我们需要做的第二件事是从javascript调用gruntfile。 唯一的麻烦是,咕unt声会在任务失败时退出整个过程。 因此,我们需要从子进程中调用它。
节点模块子进程具有三种不同的功能,能够在子进程内部运行命令:
exec
–在命令行执行命令,spawn
–在命令行上执行命令的方式不同,fork
–在子进程中运行节点模块。
第一个是exec
,最易于使用,并在第一章中进行了说明。 第二章介绍了如何使用fork以及为什么它不如exec最佳。 第三章是关于生成。
执行力
Exec在子进程中运行命令行命令。 您可以指定要在哪个目录中运行它,设置环境变量,设置超时,然后在该超时后将命令终止。 当命令完成运行时,exec调用回调并将其传递给stdout流,stderr流和命令崩溃时的错误。
除非另有配置,否则命令将在当前目录中运行。 我们希望它在tests
子目录中运行,所以我们必须指定options对象的cwd
属性: {cwd: 'tests/'}
。
stdout和stderr流内容都存储在缓冲区中。 每个缓冲区的最大大小设置为204800,如果命令产生更多输出,则exec
调用将崩溃。 这笔钱足以应付我们的小任务。 如果需要更多,则必须设置maxBuffer
options属性。
致电执行
以下代码段显示了如何从exec运行gruntfile。 该函数是异步的,并在完成之后调用whenDoneCallback
:
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);
}
注意:如果将npm安装到测试目录( 简单解决方案 ),则需要使用callNpmInstallAndGruntfile
函数而不是callGruntfile
:
function callNpmInstallAndGruntfile(filename, whenDoneCallback) {var command, options;command = "npm install";options = {cwd: 'test/'};cp.exec(command, {}, function(error, stdout, stderr) {callGruntfile(filename, whenDoneCallback);});
}
单元测试
第一节点单元测试运行成功方案,然后检查流程是否成功完成而没有失败,标准输出是否包含预期的消息以及标准错误是否为空。
成功场景单元测试:
pass: function(test) {test.expect(3);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.");test.done();});
},
第二节点单元测试运行“完全失败”方案,然后检查进程是否按预期失败。 请注意,标准错误流为空,警告被打印到标准输出中。
失败的场景单元测试:
fail_1: function(test) {test.expect(3);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.");test.done();});
}
第三次“部分故障”节点单元测试与之前的测试几乎相同。 整个测试文件可在github上找到 。
缺点
坏处:
- 必须预先设置最大缓冲区大小。
叉子
Fork在子进程中运行node.js模块,等效于在命令行上调用node <module-name>
。 Fork使用回调将标准输出和标准错误发送给调用方。 两个回调都可以被多次调用,并且调用方可以分段获取子进程的输出。
仅在需要处理任意大小的stdout和stderr或需要自定义grunt功能时,使用fork才有意义。 如果您不这样做,则exec
更易于使用。
本章分为四个子章节:
- 从javascript 呼叫grunt ,
- 读取节点模块中的命令行参数,
- 在子进程中启动节点模块,
- 编写单元测试。
呼唤咕unt声
Grunt并非以编程方式被调用。 它没有公开“公共” API,也没有对其进行记录。
我们的解决方案模仿了grunt-cli的功能,因此相对安全。 Grunt-cli与grunt核心分开分发,因此更改的可能性较小。 但是,如果确实更改,则此解决方案也必须更改。
从javascript运行咕unt声需要我们执行以下操作:
- 将gruntfile名称与其路径分开,
- 更改活动目录,
- 调用grunts
tasks
功能。
从javascript呼叫grunt:
this.runGruntfile = function(filename) {var grunt = require('grunt'), path = require('path'), directory, filename;// split filename into directory and filedirectory = path.dirname(filename);filename = path.basename(filename);//change directoryprocess.chdir(directory);//call gruntgrunt.tasks(['default'], {gruntfile:filename, color:false}, function() {console.log('done');});
};
模块参数
该模块将从命令行调用。 节点将命令行参数保留在内部
process.argv
数组:
module.exports = new function() {var filename, directory;this.runGruntfile = function(filename) {/* ... */};//get first command line argumentfilename = process.argv[2];this.runGruntfile(filename);
}();
呼叫叉
Fork具有三个参数:模块的路径,带有命令行参数的数组和options对象。 使用tests/Gruntfile-1.js
参数调用module.js
:
child = cp.fork('./module.js', ['tests/Gruntfile-1.js'], {silent: true})
silent: true
选项使返回的child
进程的stdout和stderr在父级内部可用。 如果将其设置为true,则返回的对象将提供对调用者的stdout
和stderr
流的访问。
在每个流上调用on('data', callback)
。 每次子进程向流发送某些内容时,都会调用传递的回调:
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.on('error', function(error){// handle child crashconsole.log('error: ' + error);
});
child.on('exit', function (code, signal) {// this is called after child process endedconsole.log('child process exited with code ' + code);
});
演示项目使用以下函数来调用fork和绑定回调:
/*** 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);}
}
编写测试
每个单元测试都调用callGruntfile
函数。 回调会在标准输出流中搜索所需的内容,检查退出代码是否正确,在错误流中出现错误时失败,或者在fork调用返回错误时失败。
成功场景单元测试:
pass: function(test) {var wasPassMessage = false, callbacks;test.expect(2);callbacks = {onProcessError: function(error) {test.ok(false, "Unexpected error: " + error);test.done();},onProcessExit: function(code, signal) {test.equal(code, 0, "Exit code should have been 0");test.ok(wasPassMessage, "Pass message was never sent ");test.done();},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);
}
对应于失败场景的测试几乎相同,可以在github上找到。
缺点
缺点:
- 使用的grunt函数不属于官方API。
- 子进程输出流以块而不是一个大块的形式提供。
产生
Spawn是fork和exec之间的交叉。 与exec类似,spawn能够运行可执行文件并向其传递命令行参数。 子进程输出流的处理方式与fork中的处理方式相同。 它们通过回调分段发送给父级。 因此,与使用fork一样,仅当需要任意大小的stdout或stderr时,使用spawn才有意义。
问题
产卵的主要问题发生在Windows上。 必须准确指定要运行的命令的名称。 如果使用参数grunt
调用spawn,则spawn期望可执行文件名不带后缀。 grunt.cmd
真正的grunt可执行文件grunt.cmd
。 否则, spawn
忽略Windows环境变量PATHEXT 。
循环后缀
如果要从spawn
调用grunt
,则需要执行以下操作之一:
- 针对Windows和Linux使用不同的代码,或者
- 从环境中读取
PATHEXT
并循环遍历,直到找到正确的后缀。
以下函数循环遍历PATHEXT
并将正确的文件名传递给回调:
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++) {tryExtension(extensions[i]);}
}
编写测试
一旦有了grunt命令名,就可以调用spawn
。 Spawn会触发与fork完全相同的事件,因此
callGruntfile
接受完全相同的回调对象,并将其属性绑定到子进程事件:
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 ...*/
}
测试也几乎与上一章中的测试相同。 唯一的区别是,在执行其他所有操作之前,您必须先找到grunt可执行文件名。 成功场景测试如下所示:
pass: function(test) {var wasPassMessage = false;test.expect(2);findGruntFilename(function(gruntCommand){var callbacks = {/* ... callbacks look exactly the same way as in fork ... */};callGruntfile(gruntCommand, 'gruntfile-pass.js', callbacks);});
}
完整的成功方案测试以及两个失败方案测试都可以在github上获得 。
缺点
缺点:
- Spawn会忽略
PATHEXT
后缀,需要使用自定义代码来处理它。 - 子进程输出流以块而不是一个大块的形式提供。
结论
有三种方法可以从gruntfile内部测试grunt插件。 除非您有非常强烈的理由不这样做,否则请使用exec
。
翻译自: https://www.javacodegeeks.com/2015/02/testing-grunt-plugin-from-grunt.html
grunt 插件
grunt 插件_从Grunt测试Grunt插件相关推荐
- ai二维码插件_超实用的AI脚本插件合集2.0免费分享,让你的设计快人一步
AI脚本插件合集2.0版,除了更新部分插件以及增加几款新插件外,还支持AI CC 2019了.此AI插件包目前有62款ai脚本插件,已经整合成插件面板的形式,方便在AI中调用 AI脚本插件合集说明 A ...
- 实用插件_这些实用的PR插件你知道吗?
学习PR,安装使用插件必定是绕不开的过程.一款强大的插件可以节省剪辑时间,极大的提高工作效率. 常见的视频插件类型想必大家都知道,无非是包括调色.降噪.字幕.特效四种. 调色:Magic Bullet ...
- 组件 模块 插件_播放2 –模块,插件有什么区别?
组件 模块 插件 关于Play 2模块和插件似乎有些困惑. 我想这是因为两者经常是同义词. 在Play(两个版本-1和2)中,存在明显的差异. 在本文中,我将研究什么是插件,如何在Java和Scala ...
- python处理ppt的插件_几款PPT神器插件,千万不能错过!
本文首发于公众号"干货plus" 虽然说本身PPT的功能就已经很强大了,但是,如果借助一些官方或非官方的PPT插件,就能快速制作高逼格的PPT,十分节省时间,无论是上班一族还是学生 ...
- mysql 半同步 插件_编写半同步复制插件
编写半同步复制插件 本节介绍如何使用plugin/semisyncMySQL源代码分发目录中的示例插件编写服务器端半同步复制插件.该目录包含名为rpl_semi_sync_master和的主插件和从插 ...
- python使用rpa需要什么插件_使用Python制作ArcGIS插件基础篇——工具介绍
ArcGIS从10.0开始支持addin(ArcGIS软件中又叫作加载项)的方式进行插件制作.相对于以往9.x系列,addin的无论是从使用或者编写都更加方便快捷.通过开发语言,可以制作ArcGIS ...
- c4d python 插件_好用的C4D插件都在这里了,还不赶紧收藏起来?
学C4D少不了插件,有关C4D软件的插件实在是太多太多了,下面整理了一些常用的比较酷炫的分享给大家.[找不到插件的小伙伴可以见文章末尾!!!] 一.Signal Signal是GSG出品的一款C4D程 ...
- BootStrap-CSS样式_插件_工具提示(Tooltip)插件
工具提示(Tooltip)插件 当您想要描述一个链接的时候,工具提示(Tooltip)就显得非常有用.工具提示(Tooltip)插 件是受 Jason Frame 写的 jQuery.tipsy 的启 ...
- idea打印sql的插件_[Mybatis]-[基础支持层]-插件-自定义简易SQL打印插件
该系列文章针对 Mybatis 3.5.1 版本 在 mybatis 中允许针对 SQL 在执行前后进行扩展操作,而这些扩展操作也叫做插件. 在 Mybaits 中允许用插件来拦截的方法包括: Exe ...
- 构建maven项目插件_如何构建一个Maven插件
构建maven项目插件 使用Okta的身份管理平台轻松部署您的应用程序 使用Okta的API在几分钟之内即可对任何应用程序中的用户进行身份验证,管理和保护. 今天尝试Okta. 由于其插件生态系统的普 ...
最新文章
- oracle普通用户使用dbms函数,oracle使用DBMS_SCHEDULER调度作业
- Java程序员从笨鸟到菜鸟之(六十八)细谈Spring(二)自己动手模拟spring
- 扩大缩小Linux物理分区大小
- 【小松教你手游开发】【unity实用技能】给每个GameObject的打开关闭加上一个渐变...
- 曙光:卖市场上没有的产品
- 2013 成都邀请赛
- QT程序自动拷贝所需动态库批处理
- html 随机 小游戏代码,html小游戏代码#(精选.)(3页)-原创力文档
- 一级导航,二级导航,三级导航介绍
- 高乐计算机课程,长春理工大学
- 泰坦尼克号 3D版 Titanic 3D (2012)
- ios 抓娃娃开发_可爱抓娃娃ios版_可爱抓娃娃手机版1.0.4 - 系统城
- Mac使用OBS直播配置教程|解疑答惑
- FastDFS合并存储策略
- RK3399平台开发系列讲解(IIO子系统)4.38、什么是IIO(Industrial I/O)
- 教你如何爬小说(含全代码)
- pcl::PolygonMesh简析
- linux 分析nginx日志,Linux Awk使用案例总结-nginx日志统计
- mysql sql计算经纬度
- RS485/RS232/RS422接口定义
热门文章
- jzoj6274-[NOIP提高组模拟1]梦境【贪心,堆】
- nssl1304-最大正方形【二分答案】
- jzoj1295-设计【差分约束系统,最短路】
- 洛谷P2296-寻找道路【日常图论,最短路,SPFA】
- 《信号与系统》期中总结
- 纪中A组模拟赛总结(2021.7.19)
- Java进阶学习路线
- java.sql.SQLException: The server time zone value '�й���ʱ��' is unrecognized
- vue中父组件怎么调用子组件
- 快速搭建Springboot项目的两种方式!!