基于自定义向导的C++单元测试环境自动化配置

时间:2022-04-27
本文章向大家介绍基于自定义向导的C++单元测试环境自动化配置,主要内容包括一、从向导的向导说起、二、自定义向导的编写、三、自定义向导的调试、四、自定义向导的部署、五、常见问题、基本概念、基础应用、原理机制和需要注意的事项等,并结合实例形式分析了其使用技巧,希望通过本文能帮助到大家理解应用这部分内容。

导语:相信使用过Visual Studio的小伙伴都感受过VS向导的强大,通过应用程序向导,我们可以很方便地搭建应用程序,通过代码向导,可以大大提高我们编写代码的效率。但VS的内置向导模板有时候并不能满足我们一些特殊场景的需求,比如基于第三方库的程序,每次都要手动配置一堆配置,编写重复的框架代码,Copy-Paste大法又容易犯错。本文介绍了Visual Studio扩展自定义向导的完整步骤以及核心的技术要点,通过自定义向导,可以简化许多场景下的环境配置以及框架搭建操作。

一、从向导的向导说起

VS的自定义向导,同样可以通过VS本身来开发,而且VS也为向导工程提供了向导来生成所需要的基本框架(有点类似编译器的自举)。打开VS,点击File->New Project,选择Visual C++节点(本文主要讨论C++项目的自定义向导,VS同样支持其他语言自定义向导),点击Custom Wizard,设置好相关的信息后,点击Finish,一个向导工程就创建完毕了:

这时,我们打开一个新的VS,再打开New Project页面,会发现Templates列表中已经出现了我们自定义的向导:

二、自定义向导的编写

当前的向导已经具备创建一个空的工程的能力,我们还需要了解很多相关内容才能编写出功能强大的自定义向导。

下图显示了一个命名为MyWizard的向导工程的默认文件:

总体来说,这些文件会在VS的向导引擎中被用到,大致流程及相关文件如下:

文件有点多,我们一个一个介绍。

HTML Files:这个文件夹中有一个命名为default.htm的文件,这个文件定义了我们的向导与用户的交互对话框,VS中使用Design预览下这个htm文件(VS2005的预览功能实在太差,截图使用的是VS2015的预览窗口):

这个对话框,其实是一个基于HTML的Dialog,上面的控件与布局,都是通过HTML来描述的,默认生成的页面效果如下图:

通过defaul.htm,我们可以提供一些自定义选项给用户来自定义自己的工程配置(比如各种工程配置,生成的文件的名字等各种VS能够提供的几乎所有功能),对于熟悉HTML的同学,编写这个文件几乎毫无障碍,默认的htm文件已经给出了很全面的范例(不过还是要吐槽一下布局,table套table,看得人眼花缭乱,还好VS有强大的设计编码拆分窗口)。如果你的向导不需要用户自定义配置,那么default.htm不是必须的,在建立向导工程时,去掉User interface的勾选框,这个default.htm就不会生成,用户在New Project点击OK的时候,会直接执行后边的工程创建操作。

需要特别提到的是,在HTML文件中,可以使用<SYMBOL></SYMBOL>声明一些符号:

<SYMBOL NAME='SAMPLE_CHECKBOX' TYPE=checkbox VALUE=true></SYMBOL>

这些符号对应了相关控件的默认值,比如NAME为SAMPLE_CHECKBOX的SYMBOL定义了ID为SAMPLE_CHECKBOX的值为TRUE:

<INPUT CLASS="CheckBox" TYPE="checkbox" ID="SAMPLE_CHECKBOX">

那么这个勾选框控件在展示的时候就是默认勾选的状态。

后边在脚本文件中,我们可以通过相关的语句去读取这个值(详见后边的Script Files的介绍:var bCheck= wizard.FindSymbol('SAMPLE_CHECKBOX');用户勾选了Sample checkbox,会返回true,否则就返回false。

在这里我们可以编辑HTML来设置GTEST相关的一些选项,比如是否生成测试类的某些方法以及配置、属性继承:

Image Files:这个目录可以放置我们在向导default.htm中使用的自定义图片资源。注意到最外边有一些gif文件,这些是生成的默认向导工程所使用的图片文件。

Miscellaneous Files:这里边比较重要的几个文件是:.ico、.vsdir、.vsz和Templates.inf文件。

(1).ico文件其实就是我们在New Project对话框看到的模板项目的图标,替换这个文件就可以改变在New Project对话框中的图标。

(2).vsdir是一个纯文本文件,文件内容如下:

GoogleTestProject.vsz| |GoogleTestProject|1|TODO: Wizard Description.| |6777| |GoogleTestProject.

.vsdir是Visual Studio Shell程序与向导项目中的项之间提供路由服务的文本文件,其中包含了很多的字段,以 “|”分隔(微软官方文档VSDir 文件组件给出了每个字段的含义)。其中,我们一般只需要关注TODO的部分和最后一个字段,TODO这个是我们的向导在New Project界面显示的描述字段,最后一个字段是工程的默认名称Name。

(3).vsz文件标识向导引擎并提供上下文和可选的自定义参数,它也是一个纯文本文件:

VSWIZARD 7.0
Wizard=VsWizard.VsWizardEngine.8.0

Param="WIZARD_NAME = GoogleTestProject"
Param="ABSOLUTE_PATH = D:CodeVSWizardGoogleTestProjectGoogleTestProject"
Param="FALLBACK_LCID = 1033"

头两行表示了向导版本号以及向导引擎的版本,不同版本不能混用,对于VS2005为:

VSWIZARD 7.0
Wizard=VsWizard.VsWizardEngine.8.0

VS2015则是:

VSWIZARD 7.0
Wizard=VsWizard.VsWizardEngine.14.0

自定义参数的格式为:

Param="<PARAM_NAME> = <PARAM_VALUE>"

VS中本身会有一些内置的参数,具体可查阅微软官方文档:向导 .vsz 文件中的自定义参数。

WIZARD_NAME这个定义了向导的名称,在向导PATH未指定的时候,向导会根据这个名字取查找相关文件;

ABSOLUTE_PATH告诉引擎在什么位置去查找相关的向导文件,一般在开发阶段会使用这个ABSOLUTE_PATH方便调试,后边部署的时候需要删掉或者使用RELATIVE_PATH(相对路径);

FALLBACK_LCID是指本地化相关的配置,1033表示英文(我用的是英文版VS所以这里是1033),2052表示简体中文,后边部署的时候会讲到相关的本地化问题。

(4)Templates.inf文件用来配置哪些文件需要拷贝到工程中,它也是一个纯文本文件,我们可以在Template Files下新建一些我们想要拷贝到新工程的文件,然后在Templates.inf中添加新行就可以了。inf文件本身也是一个模板文件,可以使用模板指令(如[!if] [!else]等,参见后文Template Files介绍)。

Script Files:前面说到htm文件定义了与用户的交互界面,那么default.js便是用来定义相关的事件响应逻辑,当用户在自定义配置对话框点击完成之后,后边的处理都会交给这个js文件来完成,我们重点关注OnFinish这个函数,用户点击完成之后,这个函数会被调用,默认生成的代码。很清晰,首先,通过wizard(向导的内置对象)获取用户设置的工程路径以及工程的名字,然后调用CreateCustomProject创建工程(实际上是创建了一个基本的.vcproj文件),然后添加相关的配置,设置文件分类(定义哪些属于头文件、源文件、资源文件),然后根据.inf文件渲染创建一个临时的.inf文件,将Template Files中的文件拷贝到我们新建的工程中,删除临时的.inf文件,最后保存新建的工程。

第一眼去读这些函数,你会觉得很莫名其妙,凭空就能使用的对象和函数是从哪来的?其实,对于使用js来操作VS,是属于VS扩展模型的一部分,它底层基于COM,使用C++、C#、JS等都能够进行操作(各类VS扩展插件都是基于该模型编写),具体参考微软文档:扩展 Visual Studio 环境

在VS安装目录下,比我的机器上的VS2005英文版:C:Program Files (x86)Microsoft Visual Studio 8VCVCWizards1033common.jsC:Program Files (x86)Microsoft Visual Studio 8VCVCWizards1033Script.js。文件中封装了大量的相关函数,可以直接在default.js中使用,其实在htm文件中,我们可以看到,它们会被提前加载进来:

<SCRIPT>
    var strPath = window.external.FindSymbol("PRODUCT_INSTALLATION_DIR");
    strPath += "VCWizards/";
    strPath += window.external.GetHostLocale();	
    var strScriptPath = strPath + "/Script.js";	
    var strCommonPath = strPath + "/Common.js";	
    document.scripts("INCLUDE_SCRIPT").src = strScriptPath;	
    document.scripts("INCLUDE_COMMON").src = strCommonPath;
</SCRIPT>

VS向导的向导为了示范如何使用wizard、dte等内置对象,所以生成的default.js比较复杂,其实对于一般的需求,使用Common.js中的函数就可以完成绝大部分功能,我重写了一下,以下代码框架足够使用:

// 向导完成按钮回调 必须实现

function OnFinish(selProj, selObj) 
{	
    try
    {
        // 读取工程的路径和名称        
        var strProjectPath = wizard.FindSymbol('PROJECT_PATH');	
	var strProjectName = wizard.FindSymbol('PROJECT_NAME');
        // 创建工程 并初始化通用配置
        selProj = CreateProject(strProjectName, strProjectPath);
        AddCommonConfig(selProj, strProjectName);
        SetupFilters(selProj);
        // 添加自定义的额外配置		
        AddConfig(selProj, strProjectName);
      	// 根据Templates.inf配置渲染模板
	AddFilesToNewProjectWithInfFile(selProj, strProjectName);                // 保存工程
	selProj.Object.Save();	        
    }	
    catch(e)
    {
        if (e.description.length != 0)
	    SetErrorInfo(e);		
        return e.number
    }
}
        
function AddConfig(proj, strProjectName) 
{	
    try
    {
        // Debug配置		
        var config = proj.Object.Configurations('Debug');
        var CLTool = config.Tools('VCCLCompilerTool');		
        // TODO: Add compiler settings
		
	var LinkTool = config.Tools('VCLinkerTool');
	// TODO: Add linker settings
	  
        // Release配置
	config = proj.Object.Configurations('Release');	
	var CLTool = config.Tools('VCCLCompilerTool');	
	// TODO: Add compiler settings

	var LinkTool = config.Tools('VCLinkerTool');
	// TODO: Add linker settings
    }	
    catch(e)
    {		
        throw e;
    }
}
        
// 模板文件名转换回调 必须实现
function GetTargetName(strName, strProjectName, strResPath, strHelpPath)
{	
    try
    {		
        var strTarget = strName;		
        if (strName == 'readme.txt')
            strTarget = 'ReadMe.txt';                
        // 把文件名为root的部分替换为工程名		
        if (strName.substr(0, 4) == "root")
        {
            strTarget = strProjectName + strName.substr(4);
        }
        return strTarget;
    }	
    catch(e)
    {
        throw e;
    }
}

// 文件属性设置回调 必须实现
function SetFileProperties(projfile, strName)
{	
    return false;
} 

通过查阅Common.js中的代码,我们可以学习到更多相关的对象以及函数的使用,也可以了解到inf文件一些常见的指令前缀(比如CopyOnly、OpenFile等),这里就不展开说明了。

Template Files:这个文件夹存放了我们向导需要拷贝到新工程的所有模板文件,它们可以是.h、.cpp代码文件,也可以是.ico、.txt、.rc等资源文件,任何想要生成在工程中的文件都可以放到这里,Template Files一般需要配合Templates.inf来完成相关的渲染与拷贝操作。因为用户创建一个工程的时候,难免会带上一些自定义的参数,比如使用过MFC向导的同学应该知道,我们可以指定生成的类的文件名、是否使用ATL、是动态链接还是静态链接到MFC库、使用多字节字符集还是使用Unicode字符集等,这就使得模板文件不可能是一成不变的,文件的内容必然会有变量然后有类似宏替换的操作,VS向导引擎提供了一些模板文件和Templates.inf文件中可以使用的模板指令,来完成这些需求:

指令

说明

[ !if ]

开始控制结构以检查条件。

[ !else ]

[ !if ] 控制结构的组成部分。检查另一个条件。

[ !endif ]

结束 [!if] 控制结构的定义。

[ !output ]

可通过下列两种方式使用:[ !output "string" ] 提供字符串。[ !output SYMBOL_STRING ] 提供符号 SYMBOL_STRING 的值。

[ !loop ]

可通过下列两种方式使用:[ !loop = 5 ][ !loop = NUM_OF_PAGES ],其中 NUM_OF_PAGES 是具有数值的符号。

[ !endloop ]

结束循环结构。

  • [ !output "string" ] 提供字符串。
  • [ !output SYMBOL_STRING ] 提供符号 SYMBOL_STRING 的值。

[ !loop ]可通过下列两种方式使用:

  • [ !loop = 5 ]
  • [ !loop = NUM_OF_PAGES ],其中 NUM_OF_PAGES 是具有数值的符号。

[ !endloop ]结束循环结构。

比如我们编写一个GTEST测试类的向导,用户可以有选择的生成或者不生成一些方法,那么模板文件可以这样编写:

#include <gtest/gtest.h>

class [!output PROJECT_NAME] 
: public testing::Test
{
public:
    [!output PROJECT_NAME]();
    ~[!output PROJECT_NAME]();
[!if GENERATE_SETUP_TEARDOWN]
public:	
    virtual void SetUp();	
    virtual void TearDown();
[!endif]
[!if GENERATE_SETUPCASE_TEARDOWNCASE]
public:	
    static void SetUpTestCase();	
    static void TearDownTestCase();
[!endif]
};

[!output PROJECT_NAME] 是一条输出语句,表示测试类的名称就是工程的名字。[!if GENERATE_SETUP_TEARDOWN] [!endif] 是一条判断语句,中间包含了SetUp()/TearDown()方法,如果GENERATE_SETUP_TEARDOWN这个符号(可以在htm文件中定义)为true,那么代表需要生成SetUp()/TearDown()成员函数,反之就不会生成该成员函数,对于SetUpTestCase()和TearDownTestCase()同理。

三、自定义向导的调试

向导工程其实没有编译生成的概念,因为所有的文件都是以脚本形式存在,向导的调试主要集中在default.js文件,VS强大的调试功能在此时同样能够派上用场,官方的文档对于JS调试给出的方案其实是针对ASP.NET下的,所以那些所谓的在IE中去除禁用调试选项以及设置调试cookie纯粹是在误导(我也不知道为什么微软要把这些帮助链接关联在一起,也许是机器人干的吧)。

其实调试向导很简单,新开一个VS,然后在编写向导的VS中点击Debug->Attach to Process,Attach to类型选择Script(这一步很关键,选错类型断点会无效):

进程选择新开的那个VS进程:

点击Attach,即可关联调试进程,然后在default.js中掐断点,在被调试VS中新建我们的GoogleTestProject类型工程,点击OK后,如果有断点触发,我们可以在编写向导的VS中查看各种调试信息,比如变量、函数栈等,后续的操作就跟平常调试代码一样了:

四、自定义向导的部署

自定义向导的部署本质上只需要拷贝文件到相应的目录,假设VS(以VS2005英文版为例)安装在以下目录:

C:Program Files (x86)Microsoft Visual Studio 8

那么相应的向导文件存放下以下两个目录下:

C:Program Files (x86)Microsoft Visual Studio 8VCvcprojects。
C:Program Files (x86)Microsoft Visual Studio 8VCVCWizards。

vcprojects存放.ico、.vsdir和.vsz文件,这个目录下可以新建子目录,新建的子目录会在New Project对话框中表现为一个新的分类。

例如我们将GoogleTestProject.ico、GoogleTestProject.vsdir和GoogleTestProject.vsz拷贝到以下目录:

C:Program Files (x86)Microsoft Visual Studio 8VCvcprojectsGoogleTestProject。

在New Project对话框中看到的效果就是:

VCWizards目录存放HTML、Images、Scripts、Templates等文件夹及相关文件,比如对于GoogleTestProject,我们可以把相关文件放置到以下目录:

C:Program Files (x86)Microsoft Visual Studio 8VCVCWizardsGoogleTestProject。

对于编写好的向导,在部署集成到VS中时,需要修改.vsz文件中的ABSOLUTE_PATH字段,一般直接删除掉,如果有特殊需要可以使用RELATIVE_PATH,存放的位置要跟.vsz中设定的一致,否则向导引擎会因为无法找到对应模板文件报错。如果你需要部署向导到其他版本的VS,那么需要修改.vsz中的引擎版本,另外,如果是其他的语言版本,需要修改LangID(英文是1033、中文为2052),具体可以参考微软官方文档:将向导本地化为多种语言

五、常见问题

1.为什么修改了.ico、.vsz和.vsdir文件后没有生效(比如图标没变化)?

VS在第一次创建向导工程的时候会将上述三个文件直接拷贝到VS安装目录的vcprojects目录下,你在修改这些文件的时候,往往只是修改了工程目录下的文件,VS不会帮你同步这些更改到vcprojects目录,所以修改之后需要手动copy过去。

2.调试向导出现“没有对象”错误弹窗,或者工程建好后相应文件没有拷贝或者加入到新工程?

单身狗看到这个窗口是不是受到了万点暴击伤害?额~这个问题往往是你的default.js中没有实现必须要实现的回调函数造成,比如Common.js中的CreateProject函数需GetTargetName函数获取拷贝的目标文件名称,以及SetFileProperties函数来设置文件属性,遇到这种情况,在default.js中实现这些缺失的函数就可以了(参考我前文给出的框架代码)。

3.调试的时候断点无法命中?

在编写调试期间,必须要保证你的.vsz文件描述的模板文件的目录指向向导工程目录下,也就是默认的ABSOLUTE_PATH,否则断点是不能命中的。当然,在后期部署的时候一定记得删掉ABSOLUTE_PATH或者使用RELATIVE_PATH。

4.模板文件中的中文,编辑的时候好好的,新工程中渲染完毕就成了乱码?

这很可能是因为你设置了错的LangID造成的,比如英文版VS创建的向导工程,默认的本地化语言使用的就是英语,那么在对应的模板文件中就不要使用中文。使用中文的话需要设置LangID为中文的编号2052,并且部署的时候拷贝到正确的文件夹下。

最后留给大家个问题:如果要完成向导自动化的部署,大家想的到有什么好的方法吗?