GTest源码剖析——TEST_P宏

  • GTest源码剖析TEST_P宏

    • TEST_P宏用法
    • TestWithParam 类
      • 1 TestWithParam 类定义
      • 2 WithParamInterface 模版类定义
    • INSTANTIATE_TEST_CASE_P宏
      • 1 INSTANTIATE_TEST_CASE_P宏展开
      • 2 参数生成器
        • 21 参数生成器Values
        • 22 ParamGenerator模版类
        • 23 TestParamInfo struct
        • 24 GetParamNameGen
        • 25 PrintToStringParamName
      • 3 参数化测试用例信息注册
        • 31 ParameterizedTestCaseRegistry类
        • 32 ParameterizedTestCaseInfoBase类
        • 33 ParameterizedTestCaseInfo类
        • 34 TestMetaFactoryBase类
    • TEST_P宏
      • 1 TEST_P宏展开
    • 测试用例信息注册的真正时机
      • 1 UnitTestImplRegisterParameterizedTests

        • 11 UnitTestImplRegisterParameterizedTests
        • 12 ParameterizedTestCaseRegistryRegisterTests
        • 13 ParameterizedTestCaseInfoRegisterTests
    • 参考

1 TEST_P宏用法

TEST_P宏在用法上比TEST/TEST_F宏强大很多。事实上,TEST_P宏的实现也复杂非常多。
在这里先简单介绍下TEST_P宏的使用,然后再根据它进行展开来分析源码。


//Step1:申明一个呼叫参数类,该类主要用于TEST_P宏中实现的测试逻辑使用
class CallArgs
{
public:CallArgs(bool hasAudio,bool hasVideo):_hasAudio(hasAudio),_hasVideo(hasVideo){}bool audio(){ return _hasAudio;}bool video(){ return _hasVideo;}private:bool _hasAudio;bool _hasVideo;
};//Step2:申明一个呼叫类,该类同时也是TEST_P宏的第一个参数test_case_name
//      该类继承了TestWithParam<CallArgs>模版类,从而使得CallArgs类与Call类进行了关联。
class Call: public ::testing::TestWithParam<CallArgs>
{
};//Step3: 使用INSTANTIATE_TEST_CASE_P宏,对Call类进行类相关多个的参数设置
//       这里只添加了两个参数CallArgs(true,true)和CallArgs(true,false),事实上,可以添加多大50个参数。
//       这里使用参数生成器::testing::Values,GTest定义了了很多参数生成器。
INSTANTIATE_TEST_CASE_P(VOIP, Call, ::testing::Values(CallArgs(true,true),CallArgs(true,false)) );//Step4: 编写了使用TEST_P宏实现的测试用例
//       使用了TestWithParam<CallArgs>类的GetParam()接口获取参数CallArgs
//       实际上这是两个测试用例,即该代码段会执行两个,参数分别为CallArgs(true,true)和CallArgs(true,false)
TEST_P( Call, makeCall)
{CallArgs args = GetParam();ASSERT_TRUE( makeCall(args.audio(),args.video()) );
}

2 TestWithParam 类

2.1 TestWithParam 类定义

该类仅仅是继承了Test类和WithParamInterface类,这里主要介绍一下WithParamInterface模版类。


template <typename T>
class TestWithParam : public Test, public WithParamInterface<T>
{
};

2.2 WithParamInterface 模版类定义

该类定义了ParamType,用于参数型别推导(这方面可以参考STL源码剖析)。
提供了GetParam()函数,用于TST_P宏里的实现逻辑获取参数。


template <typename T>
class WithParamInterface
{
public:typedef T ParamType;virtual ~WithParamInterface() {}const ParamType& GetParam() const {GTEST_CHECK_(parameter_ != NULL)<< "GetParam() can only be called inside a value-parameterized test "<< "-- did you intend to write TEST_P instead of TEST_F?";return *parameter_;}private:static void SetParam(const ParamType* parameter) {parameter_ = parameter;}//不太理解为何声明为static,可能是为了节约内存的考虑?//申明为static后,需要保证每次运行到不同的测试用例时,其值能够匹配。static const ParamType* parameter_;//申明了友元类ParameterizedTestFactory,使得其可以修改调用SetParam()template <class TestClass> friend class internal::ParameterizedTestFactory;
};template <typename T>
const T* WithParamInterface<T>::parameter_ = NULL;

3 INSTANTIATE_TEST_CASE_P宏

3.1 INSTANTIATE_TEST_CASE_P宏展开

  1. 定义一个获取参数生成器函数;
  2. 定义一个生成参数字符串的函数;
  3. 通过UnitTest::GetInstance()获取UnitTest类的单例;
  4. 通过parameterized_test_registry()获取UnitTest单例类的参数注册器ParameterizedTestCaseRegistry;
  5. 通过GetTestCasePatternHolder获取参数化测试用例的信息ParameterizedTestCaseInfo;
  6. 通过AddTestCaseInstantiation()添加TestCase的信息到GTest中。

#define INSTANTIATE_TEST_CASE_P(VOIP, Call, Values,... ) ) ParamGenerator<CallArgs> gtest_VOIPCall_EvalGenerator_() { return ValueArray2(); } std::string gtest_VOIPCall_EvalGenerateName_(const TestParamInfo<CallArgs>& info)
{ return GetParamNameGen<CallArgs>(info);
} int gtest_VOIPCall_dummy_  = UnitTest::GetInstance()->parameterized_test_registry(). GetTestCasePatternHolder<Call>(Call, CodeLocation(__FILE__, __LINE__))->AddTestCaseInstantiation(VOIP,&gtest_VOIPCall_EvalGenerator_, &gtest_VOIPCall_EvalGenerateName_, __FILE__, __LINE__)

3.2 参数生成器

3.2.1 参数生成器:Values

实际上Values是一组生成器,目前支持参数从1到50。


//Values
template <typename T1, typename T2>
internal::ValueArray2<T1, T2> Values(T1 v1, T2 v2) {return internal::ValueArray2<T1, T2>(v1, v2);
}//ValueArray2 模版类
//重载了operator ParamGenerator<T>() const;
template <typename T1, typename T2>
class ValueArray2
{
public:ValueArray2(T1 v1, T2 v2) : v1_(v1), v2_(v2) {}template <typename T>operator ParamGenerator<T>() const {const T array[] = {static_cast<T>(v1_), static_cast<T>(v2_)};return ValuesIn(array);}private:void operator=(const ValueArray2& other);const T1 v1_;const T2 v2_;
};

3.2.2 ParamGenerator模版类


template<typename T>
class ParamGenerator
{
public:typedef ParamIterator<T> iterator;explicit ParamGenerator(ParamGeneratorInterface<T>* impl) : impl_(impl) {}ParamGenerator(const ParamGenerator& other) : impl_(other.impl_) {}ParamGenerator& operator=(const ParamGenerator& other) {impl_ = other.impl_;return *this;}iterator begin() const { return iterator(impl_->Begin()); }iterator end() const { return iterator(impl_->End()); }private:linked_ptr<const ParamGeneratorInterface<T> > impl_;
};// ParamGeneratorInterface模版类template <typename T>
class ParamGeneratorInterface
{
public:typedef T ParamType;virtual ~ParamGeneratorInterface() {}virtual ParamIteratorInterface<T>* Begin() const = 0;virtual ParamIteratorInterface<T>* End() const = 0;
};

3.2.3 TestParamInfo struct

主要是固定参数及其index


template <class ParamType>
struct TestParamInfo
{TestParamInfo(const ParamType& a_param, size_t an_index) :param(a_param),index(an_index) {}ParamType param;size_t index;
};

3.2.4 GetParamNameGen


//默认生成包含索引的字符串
template <class ParamType>
std::string DefaultParamName(const TestParamInfo<ParamType>& info) {Message name_stream;name_stream << info.index;return name_stream.GetString();
}template <class ParamType, class ParamNameGenFunctor>
ParamNameGenFunctor GetParamNameGen(ParamNameGenFunctor func) {return func;
}template <class ParamType>
struct ParamNameGenFunc
{typedef std::string Type(const TestParamInfo<ParamType>&);
};template <class ParamType>
typename ParamNameGenFunc<ParamType>::Type *GetParamNameGen()
{return DefaultParamName;
}

3.2.5 PrintToStringParamName

struct PrintToStringParamName
{template <class ParamType>std::string operator()(const TestParamInfo<ParamType>& info) const {return PrintToString(info.param);}
};

3.3 参数化测试用例信息注册

int gtest_VOIPCall_dummy_  = UnitTest::GetInstance()->parameterized_test_registry(). GetTestCasePatternHolder<Call>(Call, CodeLocation(__FILE__, __LINE__))->AddTestCaseInstantiation(VOIP,&gtest_VOIPCall_EvalGenerator_, &gtest_VOIPCall_EvalGenerateName_, __FILE__, __LINE__)

3.3.1 ParameterizedTestCaseRegistry类

class ParameterizedTestCaseRegistry
{
public:ParameterizedTestCaseRegistry() {}//析构时销毁资源,而这些资源是在GetTestCasePatternHolder函数中创建的~ParameterizedTestCaseRegistry() {TestCaseInfoContainer::iterator it = test_case_infos_.begin();for ( ; it != test_case_infos_.end(); ++it) {delete *it;}}template <class TestCase>ParameterizedTestCaseInfo<TestCase>* GetTestCasePatternHolder(const char* test_case_name,CodeLocation code_location) {ParameterizedTestCaseInfo<TestCase>* typed_test_info = NULL;//遍历test_case_infos_查找test_case_nameTestCaseInfoContainer::iterator it = test_case_infos_.begin();for ( ; it != test_case_infos_.end(); ++it) {if ( (*it)->GetTestCaseName() == test_case_name) {//如果找到,对比TypeID是否一致;如果不一致则终止程序;if ((*it)->GetTestCaseTypeId() != GetTypeId<TestCase>()) {ReportInvalidTestCaseType(test_case_name, code_location);posix::Abort();} else {// 强制类型转换typed_test_info = CheckedDowncastToActualType<ParameterizedTestCaseInfo<TestCase> >(*it);}break;}}//如果找不到对应的test_case_name,则创建一个新的typed_test_info,并插入test_case_infos_中if (typed_test_info == NULL) {typed_test_info = new ParameterizedTestCaseInfo<TestCase>(test_case_name, code_location);test_case_infos_.push_back(typed_test_info);}return typed_test_info;}//循环遍历test_case_infos,并调用TestCase的RegisterTests()函数。void RegisterTests() {TestCaseInfoContainer::iterator it = test_case_infos_.begin();for ( ; it != test_case_infos_.end(); ++it) {(*it)->RegisterTests();}}private:typedef ::std::vector<ParameterizedTestCaseInfoBase*> TestCaseInfoContainer;TestCaseInfoContainer test_case_infos_;GTEST_DISALLOW_COPY_AND_ASSIGN_(ParameterizedTestCaseRegistry);
};

3.3.2 ParameterizedTestCaseInfoBase类

class ParameterizedTestCaseInfoBase
{
public:virtual ~ParameterizedTestCaseInfoBase() {}virtual const std::string& GetTestCaseName() const = 0;virtual TypeId GetTestCaseTypeId() const = 0;virtual void RegisterTests() = 0;protected:ParameterizedTestCaseInfoBase() {}private:GTEST_DISALLOW_COPY_AND_ASSIGN_(ParameterizedTestCaseInfoBase);
};

3.3.3 ParameterizedTestCaseInfo类

template <class TestCase>
class ParameterizedTestCaseInfo : public ParameterizedTestCaseInfoBase
{
public:typedef typename TestCase::ParamType ParamType;typedef ParamGenerator<ParamType>(GeneratorCreationFunc)();typedef typename ParamNameGenFunc<ParamType>::Type ParamNameGeneratorFunc;explicit ParameterizedTestCaseInfo(const char* name, CodeLocation code_location): test_case_name_(name), code_location_(code_location) {}virtual const std::string& GetTestCaseName() const { return test_case_name_; }//获取TestCase的ID,用于唯一标识作用,//在ParameterizedTestCaseRegistry::GetTestCasePatternHolder处有使用其进行容错判断virtual TypeId GetTestCaseTypeId() const { return GetTypeId<TestCase>(); }/*TEST_P macro uses AddTestPattern() to record informationabout a single test in a LocalTestInfo structure.test_case_name is the base name of the test case (without invocationprefix). test_base_name is the name of an individual test withoutparameter index. For the test SequenceA/FooTest.DoBar/1 FooTest istest case base name and DoBar is test base name.*/void AddTestPattern(const char* test_case_name,const char* test_base_name,TestMetaFactoryBase<ParamType>* meta_factory) {tests_.push_back(linked_ptr<TestInfo>(new TestInfo(test_case_name,test_base_name,meta_factory)));}//仅仅是把信息插入nstantiations_中int AddTestCaseInstantiation(const std::string& instantiation_name,GeneratorCreationFunc* func,ParamNameGeneratorFunc* name_func,const char* file, int line) {instantiations_.push_back(InstantiationInfo(instantiation_name, func, name_func, file, line));return 0; }virtual void RegisterTests() {//这部分代码比较多,这里先不展开。//此处的作用是真正的注册测试用例信息。//其在InitGoogleTest接口里才会得到调用。//详细参考 <<5.1.3 ParameterizedTestCaseInfo::RegisterTests()>>}  // RegisterTestsprivate:struct TestInfo {TestInfo(const char* a_test_case_base_name,const char* a_test_base_name,TestMetaFactoryBase<ParamType>* a_test_meta_factory) :test_case_base_name(a_test_case_base_name),test_base_name(a_test_base_name),test_meta_factory(a_test_meta_factory) {}const std::string test_case_base_name;const std::string test_base_name;const scoped_ptr<TestMetaFactoryBase<ParamType> > test_meta_factory;};typedef ::std::vector<linked_ptr<TestInfo> > TestInfoContainer;struct InstantiationInfo {InstantiationInfo(const std::string &name_in,GeneratorCreationFunc* generator_in,ParamNameGeneratorFunc* name_func_in,const char* file_in,int line_in): name(name_in),generator(generator_in),name_func(name_func_in),file(file_in),line(line_in) {}std::string name;GeneratorCreationFunc* generator;ParamNameGeneratorFunc* name_func;const char* file;int line;};typedef ::std::vector<InstantiationInfo> InstantiationContainer;static bool IsValidParamName(const std::string& name) {if (name.empty()){return false;}for (std::string::size_type index = 0; index < name.size(); ++index) {if (!isalnum(name[index]) && name[index] != '_'){return false;}}return true;}const std::string test_case_name_;CodeLocation code_location_;TestInfoContainer tests_;InstantiationContainer instantiations_;GTEST_DISALLOW_COPY_AND_ASSIGN_(ParameterizedTestCaseInfo);
};

3.3.4 TestMetaFactoryBase类

简单的工厂类

template <class ParamType>
class TestMetaFactoryBase
{
public:virtual ~TestMetaFactoryBase() {}virtual TestFactoryBase* CreateTestFactory(ParamType parameter) = 0;
};

4 TEST_P宏

4.1 TEST_P宏展开

eg 如前所述:

TEST_P( Call, makeCall)
{CallArgs args = GetParam();ASSERT_TRUE( makeCall(args.audio(),args.video()) );
}

展开如下:
定义了一个继承Call的Call_makeCall_Test拼接类。
与TEST宏不同的是:
1. TEST_P宏中的TestBody()函数为public;而TEST宏中的TestBody()函数为private;
2. TEST_P宏中定义了AddToRegistry()类成员用于注册信息,
而TEST宏是使用MakeAndRegisterTestInfo()接口进行信息注册。

# define TEST_P(Call, makeCall)// Step1:申明一个拼接类
class Call_makeCall_Test : public Call
{
public:Call_makeCall_Test {} virtual void TestBody(); private://1: 通过UnitTest::GetInstance()获取UnitTest类的单例;//2: 通过parameterized_test_registry()获取UnitTest单例类的ParameterizedTestCaseRegistry;//3: 通过GetTestCasePatternHolder获取参数化测试用例的信息ParameterizedTestCaseInfo;//   因为在INSTANTIATE_TEST_CASE_P处调用时已创建ParameterizedTestCaseInfo,故此次仅是获取;//4: 通过ParameterizedTestCaseInfo类的AddTestPattern函数添加TestCase的信息到GTest中。//   与INSTANTIATE_TEST_CASE_P宏调用AddTestCaseInstantiation函数不同的是,//   这里调用AddTestPattern函数添加了具体的测试用例信息,即如“makeCall”。static int AddToRegistry() { ::testing::UnitTest::GetInstance()->parameterized_test_registry(). GetTestCasePatternHolder<Call>(Call, CodeLocation(__FILE__, __LINE__))->AddTestPattern(Call,makeCall,new TestMetaFactory<Call_makeCall_Test>());return 0; } static int gtest_registering_dummy_ ; GTEST_DISALLOW_COPY_AND_ASSIGN_(Call_makeCall_Test);
}; // Step2:注册测试用例信息
int Call_makeCall_Test::gtest_registering_dummy_ = Call_makeCall_Test::AddToRegistry(); // Step3:对TestBody()进行实现
void Call_makeCall_Test::TestBody()
{CallArgs args = GetParam();ASSERT_TRUE( makeCall(args.audio(),args.video()) );
}

5 测试用例信息注册的真正时机

事实上,到目前为止,测试用例的信息仍未真正的注册。
TEST宏比较分立,所以它是有一个用例注册一次。与TEST/TEST_P宏不同的是,TEST_P宏需要和INSTANTIATE_TEST_CASE_P宏进行合作,所以其实际上真正注册信息的时机在initGoogleTest函数里实现的。因为initGoogleTest函数在main函数里面调用,此时TEST_P宏和INSTANTIATE_TEST_CASE_P宏的信息已经全部加载完毕,此时再统一进行信息注册。


InitGoogleTest();
==>InitGoogleTestImpl();==>GetUnitTestImpl()->PostFlagParsingInit();==>UnitTestImpl::RegisterParameterizedTests();

5.1 UnitTestImpl::RegisterParameterizedTests()

5.1.1 UnitTestImpl::RegisterParameterizedTests()

通过parameterized_tests_registered_确保该函数只调用一次
其实现是通过调用ParameterizedTestCaseRegistry类的RegisterTests()函数实现。


void UnitTestImpl::RegisterParameterizedTests()
{
#if GTEST_HAS_PARAM_TESTif (!parameterized_tests_registered_) {parameterized_test_registry_.RegisterTests();parameterized_tests_registered_ = true;}
#endif
}

5.1.2 ParameterizedTestCaseRegistry::RegisterTests()

  1. 在ParameterizedTestCaseRegistry类中,所有测试用例信息都保存在test_case_infos_,是INSTANTIATE_TEST_CASE_P宏调用GetTestCasePatternHolder函数,创建并保存的信息数据。

ParameterizedTestCaseRegistry::RegisterTests()的实现是,循环遍历test_case_infos_数组,并逐次调用ParameterizedTestCaseInfoBase::RegisterTests()函数进行注册。


void ParameterizedTestCaseRegistry::RegisterTests()
{//typedef ::std::vector<ParameterizedTestCaseInfoBase*> TestCaseInfoContainer;TestCaseInfoContainer::iterator it = test_case_infos_.begin();for ( ; it != test_case_infos_数组,.end(); ++it) {(*it)->RegisterTests();}
}

5.1.3 ParameterizedTestCaseInfo::RegisterTests()

事实上,ParameterizedTestCaseInfoBase是个虚类,提供相关接口,在GTest主要是ParameterizedTestCaseInfo类进行实现。我们真实的注册也是通过ParameterizedTestCaseInfo::RegisterTests()接口完成的。

virtual void ParameterizedTestCaseInfo::RegisterTests()
{//循环遍历tests_,tests_值是在TEST_P调用AddTestPattern函数添加的。typename TestInfoContainer::iterator test_it = tests_.begin();for ( ; test_it != tests_.end(); ++test_it) {linked_ptr<TestInfo> test_info = *test_it;typename InstantiationContainer::iterator gen_it = instantiations_.begin();//循环遍历instantiations_//其值是INSTANTIATE_TEST_CASE_P宏调用AddTestCaseInstantiation()函数添加的。for ( ; gen_it != instantiations_.end(); ++gen_it) {const std::string& instantiation_name = gen_it->name;ParamGenerator<ParamType> generator((*gen_it->generator)());ParamNameGeneratorFunc* name_func = gen_it->name_func;const char* file = gen_it->file;int line = gen_it->line;//此处进行拼接test_case_name;//eg:instantiation_name :"VOIP"//  :test_info->test_case_base_name: "Call"//  ===>test_case_name = "VOIP/Call"std::string test_case_name;if ( !instantiation_name.empty() ){test_case_name = instantiation_name + "/";}test_case_name += test_info->test_case_base_name;size_t i = 0;std::set<std::string> test_param_names;//循环遍历参数生成器,并拼接测试用例名//eg: ==> "makeCall/0"==> "makeCall/1"typename ParamGenerator<ParamType>::iterator param_it = generator.begin();for ( ; param_it != generator.end(); ++param_it, ++i) {Message test_name_stream;std::string param_name = name_func(TestParamInfo<ParamType>(*param_it, i));GTEST_CHECK_(IsValidParamName(param_name))<< "Parameterized test name '" << param_name<< "' is invalid, in " << file<< " line " << line << std::endl;GTEST_CHECK_(test_param_names.count(param_name) == 0)<< "Duplicate parameterized test name '" << param_name<< "', in " << file << " line " << line << std::endl;test_param_names.insert(param_name);test_name_stream << test_info->test_base_name << "/" << param_name;//调用MakeAndRegisterTestInfo进行信息注册,该函数分析详见《GTest源码剖析——TEST宏》处MakeAndRegisterTestInfo(test_case_name.c_str(),test_name_stream.GetString().c_str(),NULL,  // No type parameter.PrintToString(*param_it).c_str(),code_location_,GetTestCaseTypeId(),TestCase::SetUpTestCase,TestCase::TearDownTestCase,test_info->test_meta_factory->CreateTestFactory(*param_it));}  }  }
}  // RegisterTests

如上,正式完成了TEST_P宏的信息注册。

6 参考

github: googletest


ZhaiPillar
2017-09-16

GTest源码剖析(四)——TEST_P宏相关推荐

  1. linux源码剖析四 built-in.o 文件编译生成过程

    vmliux 依赖 vmlinux-deps,而 vmlinux-deps=$(KBUILD_LDS) $(KBUILD_VMLINUX_INIT) $(KBUILD_VMLINUX_MAIN), K ...

  2. 源码 状态机_阿里中间件seata源码剖析七:saga模式实现

    saga模式是分布式事务中使用比较多的一种模式,他主要应用在长流程的服务,对一个全局事务,如果某个节点抛出了异常,则从这个节点往前依次回滚或补偿事务.今天我们就来看看它的源码实现. 状态机初始化 在之 ...

  3. 阿里中间件seata源码剖析六:TCC模式中2阶段提交实现

    目录 TM通知TC事务状态 TC通知RM分支事务提交 RM处理TC提交事务请求 总结 上篇文章中,我们以TCC模式的demo为例,讲解了seata中全局事务的开启.在这个demo中,TM作为一个全局事 ...

  4. GDAL源码剖析(四)之命令行程序说明二

    接博客GDAL源码剖析(四)之命令行程序说明一http://blog.csdn.net/liminlu0314/article/details/6978589 其中有个nearblack,gdalbu ...

  5. Laravel源码剖析之请求的处理上(四)

    上篇讲了make方法-->Laravel源码剖析之make详解(三)_Attitude_do_it的博客-CSDN博客, 根据make方法的分析可以得出: $kernel = $app-> ...

  6. boost源码剖析之:多重回调机制signal(上)

    boost源码剖析之:多重回调机制signal(上) 刘未鹏 C++的罗浮宫(http://blog.csdn.net/pongba) boost库固然是技术的宝库,却更是思想的宝库.大多数程序员都知 ...

  7. 《STL源码剖析》相关面试题总结

    一.STL简介 STL提供六大组件,彼此可以组合套用: 容器 容器就是各种数据结构,我就不多说,看看下面这张图回忆一下就好了,从实现角度看,STL容器是一种class template. 算法 各种常 ...

  8. Chrome源码剖析、上--多线程模型、进程通信、进程模型

    Chrome源码剖析.上 原著:duguguiyu. 整理:July. 时间:二零一一年四月二日. 出处:http://blog.csdn.net/v_JULY_v. 说明:此Chrome源码剖析很大 ...

  9. Chrome源码剖析 上--多线程模型 进程通信 进程模型

    分享一下我老师大神的人工智能教程!零基础,通俗易懂!http://blog.csdn.net/jiangjunshow 也欢迎大家转载本篇文章.分享知识,造福人民,实现我们中华民族伟大复兴! Chro ...

最新文章

  1. SAP RETAIL 参考PO创建分配表之一
  2. java怎样读txt文件_【后端开辟】java怎样读写txt文件?
  3. 在SQL Server引用dll的流程
  4. mysql 存储过程与存储函数
  5. div中插入图片_Web前端开发基础知识,设置网页背景图,如何在网页中插入图片...
  6. [转]js 取得 Unix时间戳(Unix timestamp)
  7. (JAVA)Random类
  8. Android 剪切板
  9. get和post方式传递参数
  10. 苹果因不带充电器被罚款200万美元;杨笠代言英特尔被抵制,品牌方连夜下架;Linux考虑加入对Rust的支持 | 极客头条...
  11. 专访谷歌医疗AI LilyPeng:吴恩达的医生“失业论”不妥
  12. python导入上级目录的模块
  13. 关于地统计的一些知识点
  14. 应用密码学笔记第五章-第六章
  15. WPS Excel快捷键
  16. 产业链图谱:2021年中国智能制造业产业链图谱|产业链全景图
  17. idea项目配置jsp模板
  18. 职场002:什么是可迁移能力
  19. IGMP协议(IGMPv1、IGMPv2、IGMPv3)
  20. Life Long Learning论文初探————Online Fast Adaptation and Knowledge Accumulation(OSAKA)

热门文章

  1. MQL4常见错误代号及处理
  2. cvx实数变量_cvx 官网教程
  3. javaweb农产品网络交易平台设计springboot+ssm
  4. Linux系统安装jdk11
  5. 来看看这些“高科技奶嘴”,是你的“快乐老家”吗?
  6. druid连接池参数
  7. 00HTML5学习之互联网与万维网介绍
  8. 上海西门子培训-第三天(周二)
  9. BPM工作流 前端JS 处理细节(二)
  10. Java初级基础之代码块