玩转Google开源C++单元测试框架Google Test系列(gtest)之七 - 深入解析gtest

news2024/11/24 13:37:51

一、前言

“深入解析”对我来说的确有些难度,所以我尽量将我学习到和观察到的gtest内部实现介绍给大家。本文算是抛砖引玉吧,只能是对gtest的整体结构的一些介绍,想要了解更多细节最好的办法还是看gtest源码,如果你看过gtest源码,你会发现里面的注释非常的详细!好了,下面就开始了解gtest吧。

二、从TEST宏开始

前面的文章已经介绍过TEST宏的用法了,通过TEST宏,我们可以非法简单、方便的编写测试案例,比如:

TEST(FooTest, Demo)
{
    EXPECT_EQ(1, 1);
}

我们先不去看TEST宏的定义,而是先使用/P参数将TEST展开。如果使用的是Vistual Studio的话:

1. 选中需要展开的代码文件,右键 - 属性 - C/C++ - Preprocessor

2. Generate Preprocessed File 设置 Without Line Numbers (/EP /P) 或 With Line Numbers (/P)

3. 关闭属性对话框,右键选中需要展开的文件,右键菜单中点击:Compile

编译过后,会在源代码目录生成一个后缀为.i的文件,比如我对上面的代码进行展开,展开后的内容为:

class FooTest_Demo_Test : public ::testing::Test 
{
public: 
    FooTest_Demo_Test() {}
private: 
    virtual void TestBody();
    static ::testing::TestInfo* const test_info_;
    FooTest_Demo_Test(const FooTest_Demo_Test &);
    void operator=(const FooTest_Demo_Test &);
};

::testing::TestInfo* const FooTest_Demo_Test 
    ::test_info_ = 
        ::testing::internal::MakeAndRegisterTestInfo( 
            "FooTest", "Demo", "", "",
            (::testing::internal::GetTestTypeId()),
            ::testing::Test::SetUpTestCase,
            ::testing::Test::TearDownTestCase,
            new ::testing::internal::TestFactoryImpl< FooTest_Demo_Test>);

void FooTest_Demo_Test::TestBody()
{
    switch (0)
    case 0:
        if (const ::testing::AssertionResult 
                gtest_ar = 
                    (::testing::internal:: EqHelper<(sizeof(::testing::internal::IsNullLiteralHelper(1)) == 1)>::Compare("1", "1", 1, 1)))
            ;
        else 
            ::testing::internal::AssertHelper(
                ::testing::TPRT_NONFATAL_FAILURE,
                ".\\gtest_demo.cpp",
                9,
                gtest_ar.failure_message()
                ) = ::testing::Message();
}

展开后,我们观察到:

1. TEST宏展开后,是一个继承自testing::Test的类。

2. 我们在TEST宏里面写的测试代码,其实是被放到了类的TestBody方法中。

3. 通过静态变量test_info_,调用MakeAndRegisterTestInfo对测试案例进行注册。

如下图:

上面关键的方法就是MakeAndRegisterTestInfo了,我们跳到MakeAndRegisterTestInfo函数中:

// 创建一个 TestInfo 对象并注册到 Google Test;
// 返回创建的TestInfo对象
//
// 参数:
//
//   test_case_name:            测试案例的名称
//   name:                           测试的名称
//   test_case_comment:       测试案例的注释信息
//   comment:                      测试的注释信息
//   fixture_class_id:             test fixture类的ID
//   set_up_tc:                    事件函数SetUpTestCases的函数地址
//   tear_down_tc:               事件函数TearDownTestCases的函数地址
//   factory:                        工厂对象,用于创建测试对象(Test)
TestInfo* MakeAndRegisterTestInfo(
    const char* test_case_name, const char* name,
    const char* test_case_comment, const char* comment,
    TypeId fixture_class_id,
    SetUpTestCaseFunc set_up_tc,
    TearDownTestCaseFunc tear_down_tc,
    TestFactoryBase* factory) {
  TestInfo* const test_info =
      new TestInfo(test_case_name, name, test_case_comment, comment,
                   fixture_class_id, factory);
  GetUnitTestImpl()->AddTestInfo(set_up_tc, tear_down_tc, test_info);
  return test_info;
}

我们看到,上面创建了一个TestInfo对象,然后通过AddTestInfo注册了这个对象。TestInfo对象到底是一个什么样的东西呢?

TestInfo对象主要用于包含如下信息:

1. 测试案例名称(testcase name)

2. 测试名称(test name)

3. 该案例是否需要执行

4. 执行案例时,用于创建Test对象的函数指针

5. 测试结果

我们还看到,TestInfo的构造函数中,非常重要的一个参数就是工厂对象,它主要负责在运行测试案例时创建出Test对象。我们看到我们上面的例子的factory为:

new ::testing::internal::TestFactoryImpl< FooTest_Demo_Test>

我们明白了,Test对象原来就是TEST宏展开后的那个类的对象(FooTest_Demo_Test),再看看TestFactoryImpl的实现:

template <class TestClass>
class TestFactoryImpl : public TestFactoryBase {
public:
    virtual Test* CreateTest() { return new TestClass; }
};

这个对象工厂够简单吧,嗯,Simple is better。当我们需要创建一个测试对象(Test)时,调用factory的CreateTest()方法就可以了。

创建了TestInfo对象后,再通过下面的方法对TestInfo对象进行注册:

GetUnitTestImpl()->AddTestInfo(set_up_tc, tear_down_tc, test_info);

GetUnitTestImpl()是获取UnitTestImpl对象:

inline UnitTestImpl* GetUnitTestImpl() {
    return UnitTest::GetInstance()->impl();
}

其中UnitTest是一个单件(Singleton),整个进程空间只有一个实例,通过UnitTest::GetInstance()获取单件的实例。上面的代码看到,UnitTestImpl对象是最终是从UnitTest对象中获取的。那么UnitTestImpl到底是一个什么样的东西呢?可以这样理解:

UnitTestImpl是一个在UnitTest内部使用的,为执行单元测试案例而提供了一系列实现的那么一个类。(自己归纳的,可能不准确)

我们上面的AddTestInfo就是其中的一个实现,负责注册TestInfo实例:

// 添加TestInfo对象到整个单元测试中
//
// 参数:
//
//   set_up_tc:      事件函数SetUpTestCases的函数地址
//   tear_down_tc: 事件函数TearDownTestCases的函数地址
//   test_info:        TestInfo对象
void AddTestInfo(Test::SetUpTestCaseFunc set_up_tc,
               Test::TearDownTestCaseFunc tear_down_tc,
               TestInfo * test_info) {
// 处理死亡测试的代码,先不关注它
if (original_working_dir_.IsEmpty()) {
    original_working_dir_.Set(FilePath::GetCurrentDir());
    if (original_working_dir_.IsEmpty()) {
        printf("%s\n", "Failed to get the current working directory.");
        abort();
    }
}
// 获取或创建了一个TestCase对象,并将testinfo添加到TestCase对象中。
GetTestCase(test_info->test_case_name(),
            test_info->test_case_comment(),
            set_up_tc,
            tear_down_tc)->AddTestInfo(test_info);
}

我们看到,TestCase对象出来了,并通过AddTestInfo添加了一个TestInfo对象。这时,似乎豁然开朗了:

1. TEST宏中的两个参数,第一个参数testcase_name,就是TestCase对象的名称,第二个参数test_name就是Test对象的名称。而TestInfo包含了一个测试案例的一系列信息。

2. 一个TestCase对象对应一个或多个TestInfo对象。

我们来看看TestCase的创建过程(UnitTestImpl::GetTestCase):

// 查找并返回一个指定名称的TestCase对象。如果对象不存在,则创建一个并返回
//
// 参数:
//
//   test_case_name:    测试案例名称
//   set_up_tc:            事件函数SetUpTestCases的函数地址
//   tear_down_tc:       事件函数TearDownTestCases的函数地址
TestCase* UnitTestImpl::GetTestCase(const char* test_case_name,
                                    const char* comment,
                                    Test::SetUpTestCaseFunc set_up_tc,
                                    Test::TearDownTestCaseFunc tear_down_tc) {
  // 从test_cases里查找指定名称的TestCase
    internal::ListNode<TestCase*>* node = test_cases_.FindIf(
        TestCaseNameIs(test_case_name));

    if (node == NULL) {
        // 没找到,我们来创建一个
        TestCase* const test_case =
            new TestCase(test_case_name, comment, set_up_tc, tear_down_tc);

        // 判断是否为死亡测试案例
        if (internal::UnitTestOptions::MatchesFilter(String(test_case_name),
                                                 kDeathTestCaseFilter)) {
            // 是的话,将该案例插入到最后一个死亡测试案例后
            node = test_cases_.InsertAfter(last_death_test_case_, test_case);
            last_death_test_case_ = node;
        } else {
            // 否则,添加到test_cases最后。
            test_cases_.PushBack(test_case);
            node = test_cases_.Last();
        }
    }

    // 返回TestCase对象
    return node->element();
}

三、回过头看看TEST宏的定义

#define TEST(test_case_name, test_name)\
    GTEST_TEST_(test_case_name, test_name, \
              ::testing::Test, ::testing::internal::GetTestTypeId())

同时也看看TEST_F宏

#define TEST_F(test_fixture, test_name)\
    GTEST_TEST_(test_fixture, test_name, test_fixture, \
              ::testing::internal::GetTypeId<test_fixture>())

都是使用了GTEST_TEST_宏,在看看这个宏如何定义的:

#define GTEST_TEST_(test_case_name, test_name, parent_class, parent_id)\
class GTEST_TEST_CLASS_NAME_(test_case_name, test_name) : public parent_class {\
public:\
    GTEST_TEST_CLASS_NAME_(test_case_name, test_name)() {}\
private:\
    virtual void TestBody();\
    static ::testing::TestInfo* const test_info_;\
    GTEST_DISALLOW_COPY_AND_ASSIGN_(\
        GTEST_TEST_CLASS_NAME_(test_case_name, test_name));\
};\
\
::testing::TestInfo* const GTEST_TEST_CLASS_NAME_(test_case_name, test_name)\
    ::test_info_ =\
        ::testing::internal::MakeAndRegisterTestInfo(\
            #test_case_name, #test_name, "", "", \
            (parent_id), \
            parent_class::SetUpTestCase, \
            parent_class::TearDownTestCase, \
            new ::testing::internal::TestFactoryImpl<\
                GTEST_TEST_CLASS_NAME_(test_case_name, test_name)>);\
void GTEST_TEST_CLASS_NAME_(test_case_name, test_name)::TestBody()

不需要多解释了,和我们上面展开看到的差不多,不过这里比较明确的看到了,我们在TEST宏里写的就是TestBody里的东西。这里再补充说明一下里面的GTEST_DISALLOW_COPY_AND_ASSIGN_宏,我们上面的例子看出,这个宏展开后:

FooTest_Demo_Test(const FooTest_Demo_Test &);
void operator=(const FooTest_Demo_Test &);

正如这个宏的名字一样,它是用于防止对对象进行拷贝和赋值操作的。

四、再来了解RUN_ALL_TESTS宏

我们的测试案例的运行就是通过这个宏发起的。RUN_ALL_TEST的定义非常简单:

#define RUN_ALL_TESTS()\
    (::testing::UnitTest::GetInstance()->Run())

我们又看到了熟悉的::testing::UnitTest::GetInstance(),看来案例的执行时从UnitTest的Run方法开始的,我提取了一些Run中的关键代码,如下:

int UnitTest::Run() {
    __try {
        return impl_->RunAllTests();
    } __except(internal::UnitTestOptions::GTestShouldProcessSEH(
        GetExceptionCode())) {
        printf("Exception thrown with code 0x%x.\nFAIL\n", GetExceptionCode());
        fflush(stdout);
        return 1;
    }
    return impl_->RunAllTests();
}

我们又看到了熟悉的impl(UnitTestImpl),具体案例该怎么执行,还是得靠UnitTestImpl。

int UnitTestImpl::RunAllTests() {

    // ...

    printer->OnUnitTestStart(parent_);

    // 计时
    const TimeInMillis start = GetTimeInMillis();

    printer->OnGlobalSetUpStart(parent_);
    // 执行全局的SetUp事件
    environments_.ForEach(SetUpEnvironment);
    printer->OnGlobalSetUpEnd(parent_);

    // 全局的SetUp事件执行成功的话
    if (!Test::HasFatalFailure()) {
        // 执行每个测试案例
        test_cases_.ForEach(TestCase::RunTestCase);
    }

    // 执行全局的TearDown事件
    printer->OnGlobalTearDownStart(parent_);
    environments_in_reverse_order_.ForEach(TearDownEnvironment);
    printer->OnGlobalTearDownEnd(parent_);

    elapsed_time_ = GetTimeInMillis() - start;

    // 执行完成
    printer->OnUnitTestEnd(parent_);

    // Gets the result and clears it.
    if (!Passed()) {
      failed = true;
    }
    ClearResult();

    // 返回测试结果
    return failed ? 1 : 0;
}

上面,我们很开心的看到了我们前面讲到的全局事件的调用。environments_是一个Environment的链表结构(List),它的内容是我们在main中通过:

testing::AddGlobalTestEnvironment(new FooEnvironment);

添加进去的。test_cases_我们之前也了解过了,是一个TestCase的链表结构(List)。gtest实现了一个链表,并且提供了一个Foreach方法,迭代调用某个函数,并将里面的元素作为函数的参数:

template <typename F>  // F is the type of the function/functor
void ForEach(F functor) const {
    for ( const ListNode<E> * node = Head();
          node != NULL;
          node = node->next() ) {
      functor(node->element());
    }
}

因此,我们关注一下:environments_.ForEach(SetUpEnvironment),其实是迭代调用了SetUpEnvironment函数:

static void SetUpEnvironment(Environment* env) { env->SetUp(); }

最终调用了我们定义的SetUp()函数。

再看看test_cases_.ForEach(TestCase::RunTestCase)的TestCase::RunTestCase实现:

static void RunTestCase(TestCase * test_case) { test_case->Run(); }

再看TestCase的Run实现:

void TestCase::Run() {
    if (!should_run_) return;

    internal::UnitTestImpl* const impl = internal::GetUnitTestImpl();
    impl->set_current_test_case(this);

    UnitTestEventListenerInterface * const result_printer =
    impl->result_printer();

    result_printer->OnTestCaseStart(this);
    impl->os_stack_trace_getter()->UponLeavingGTest();
    // 哈!SetUpTestCases事件在这里调用
    set_up_tc_();

    const internal::TimeInMillis start = internal::GetTimeInMillis();
    // 嗯,前面分析的一个TestCase对应多个TestInfo,因此,在这里迭代对TestInfo调用RunTest方法
    test_info_list_->ForEach(internal::TestInfoImpl::RunTest);
    elapsed_time_ = internal::GetTimeInMillis() - start;

    impl->os_stack_trace_getter()->UponLeavingGTest();
    // TearDownTestCases事件在这里调用
    tear_down_tc_();
    result_printer->OnTestCaseEnd(this);
    impl->set_current_test_case(NULL);
}

第二种事件机制又浮出我们眼前,非常兴奋。可以看出,SetUpTestCases和TearDownTestCaess是在一个TestCase之前和之后调用的。接着看test_info_list_->ForEach(internal::TestInfoImpl::RunTest):

static void RunTest(TestInfo * test_info) {
    test_info->impl()->Run();
}

哦?TestInfo也有一个impl?看来我们之前漏掉了点东西,和UnitTest很类似,TestInfo内部也有一个主管各种实现的类,那就是TestInfoImpl,它在TestInfo的构造函数中创建了出来(还记得前面讲的TestInfo的创建过程吗?):

TestInfo::TestInfo(const char* test_case_name,
                   const char* name,
                   const char* test_case_comment,
                   const char* comment,
                   internal::TypeId fixture_class_id,
                   internal::TestFactoryBase* factory) {
    impl_ = new internal::TestInfoImpl(this, test_case_name, name,
                                     test_case_comment, comment,
                                     fixture_class_id, factory);
}

因此,案例的执行还得看TestInfoImpl的Run()方法,同样,我简化一下,只列出关键部分的代码:

void TestInfoImpl::Run() {

    // ...

    UnitTestEventListenerInterface* const result_printer =
        impl->result_printer();
    result_printer->OnTestStart(parent_);
    // 开始计时
    const TimeInMillis start = GetTimeInMillis();

    Test* test = NULL;

    __try {
        // 我们的对象工厂,使用CreateTest()生成Test对象
        test = factory_->CreateTest();
    } __except(internal::UnitTestOptions::GTestShouldProcessSEH(
        GetExceptionCode())) {
        AddExceptionThrownFailure(GetExceptionCode(),
                              "the test fixture's constructor");
        return;

    }

    // 如果Test对象创建成功

    if (!Test::HasFatalFailure()) {

        // 调用Test对象的Run()方法,执行测试案例

        test->Run();
    }

    // 执行完毕,删除Test对象
    impl->os_stack_trace_getter()->UponLeavingGTest();
    delete test;
    test = NULL;

    // 停止计时
    result_.set_elapsed_time(GetTimeInMillis() - start);
    result_printer->OnTestEnd(parent_);

}

上面看到了我们前面讲到的对象工厂fatory,通过fatory的CreateTest()方法,创建Test对象,然后执行案例又是通过Test对象的Run()方法:

void Test::Run() {
    if (!HasSameFixtureClass()) return;

    internal::UnitTestImpl* const impl = internal::GetUnitTestImpl();
    impl->os_stack_trace_getter()->UponLeavingGTest();
    __try {
        // Yeah!每个案例的SetUp事件在这里调用
        SetUp();
    } __except(internal::UnitTestOptions::GTestShouldProcessSEH(
        GetExceptionCode())) {
        AddExceptionThrownFailure(GetExceptionCode(), "SetUp()");
    }

    // We will run the test only if SetUp() had no fatal failure.
    if (!HasFatalFailure()) {
        impl->os_stack_trace_getter()->UponLeavingGTest();
        __try {
            // 哈哈!!千辛万苦,我们定义在TEST宏里的东西终于被调用了!
            TestBody();
        } __except(internal::UnitTestOptions::GTestShouldProcessSEH(
            GetExceptionCode())) {
            AddExceptionThrownFailure(GetExceptionCode(), "the test body");
        }
    }

    impl->os_stack_trace_getter()->UponLeavingGTest();
    __try {
        // 每个案例的TearDown事件在这里调用
        TearDown();
    } __except(internal::UnitTestOptions::GTestShouldProcessSEH(
        GetExceptionCode())) {
        AddExceptionThrownFailure(GetExceptionCode(), "TearDown()");
    }
}

上面的代码里非常极其以及特别的兴奋的看到了执行测试案例的前后事件,测试案例执行TestBody()的代码。仿佛整个gtest的流程在眼前一目了然了。

四、总结

本文通过分析TEST宏和RUN_ALL_TEST宏,了解到了整个gtest运作过程,可以说整个过程简洁而优美。之前读《代码之美》,感触颇深,现在读过gtest代码,再次让我感触深刻。记得很早前,我对设计的理解是“功能越强大越好,设计越复杂越好,那样才显得牛”,渐渐得,我才发现,简单才是最好。我曾总结过自己写代码的设计原则:功能明确,设计简单。了解了gtest代码后,猛然发现gtest不就是这样吗,同时gtest也给了我很多惊喜,因此,我对gtest的评价是:功能强大,设计简单,使用方便。

总结一下gtest里的几个关键的对象:

1. UnitTest 单例,总管整个测试,包括测试环境信息,当前执行状态等等。

2. UnitTestImpl UnitTest内部具体功能的实现者。

3. Test    我们自己编写的,或通过TEST,TEST_F等宏展开后的Test对象,管理着测试案例的前后事件,具体的执行代码TestBody。

4. TestCase 测试案例对象,管理着基于TestCase的前后事件,管理内部多个TestInfo。

5. TestInfo  管理着测试案例的基本信息,包括Test对象的创建方法。

6. TestInfoImpl TestInfo内部具体功能的实现者 。

本文还有很多gtest的细节没有分析到,比如运行参数,死亡测试,跨平台处理,断言的宏等等,希望读者自己把源码下载下来慢慢研究。如本文有错误之处,也请大家指出,谢谢!

系列链接:

1.玩转Google开源C++单元测试框架Google Test系列(gtest)之一 - 初识gtest

2.玩转Google开源C++单元测试框架Google Test系列(gtest)之二 - 断言

3.玩转Google开源C++单元测试框架Google Test系列(gtest)之三 - 事件机制

4.玩转Google开源C++单元测试框架Google Test系列(gtest)之四 - 参数化

5.玩转Google开源C++单元测试框架Google Test系列(gtest)之五 - 死亡测试

6.玩转Google开源C++单元测试框架Google Test系列(gtest)之六 - 运行参数

7.玩转Google开源C++单元测试框架Google Test系列(gtest)之七 - 深入解析gtest

8.玩转Google开源C++单元测试框架Google Test系列(gtest)之八 - 打造自己的单元测试框架

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/530392.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

麒麟操作系统软件更新灾难连篇之一:中文输入法消失

今天在麒麟操作系统开QQ总是过一会儿就闪退&#xff0c;于是进软件商店看看是否有更新。 真是不看不知道&#xff0c;一看吓一跳&#xff0c;居然有几十个软件更新&#xff0c;照常理&#xff0c;软件升级后应该是更加好用&#xff0c;于是先把QQ、五笔字型、搜狗输入法等几个常…

centos7.9搭建redis6.0.6哨兵模式

redis6.0.6哨兵模式搭建文档 1.准备工作1.1 ip规划安装依赖&#xff08;三台机器都操作&#xff09;1.3 gcc升级&#xff08;三台机器都操作&#xff09; 2.安装redis&#xff08;三台机器都操作&#xff09;2.1 获取安装包2.2 解压2.3 编译2.4 验证上一步是否正确2.5 安装2.6…

Windows10安装二进制Mysql-5.7.41和汉化

1.创建my.ini [mysqld] ##skip-grant-tables1 port 3306 basedirD:/webStudy/mysql-5.7.41 datadirE:/adata/mysqlData max_connections200 character-set-serverutf8 default-storage-engineINNODB sql_modeNO_ENGINE_SUBSTITUTION,STRICT_TRANS_TABLES [mysql] default-char…

Liunx基础命令 - which命令

which命令 – 查找命令文件 ​ which命令的功能是用于查找命令文件&#xff0c;能够快速搜索二进制程序所对应的位置。如果我们既不关心同名文件&#xff08;find与locate&#xff09;&#xff0c;也不关心命令所对应的源代码和帮助文件&#xff08;whereis&#xff09;&#…

C++中类的静态成员变量与静态成员函数

static声明为静态的&#xff0c;称为静态成员。 不管这个类创建了多少个对象&#xff0c;静态成员只有一个拷贝&#xff0c;这个拷贝被所有属于这个类的对象共享。 静态成员 属于类 而不是对象。 静态变量&#xff0c;是在编译阶段就分配空间&#xff0c;对象还没有创建时&…

ARM-栈帧(一)

ARM 栈帧 本系列均以 corter-A7(armv7-a) 为例 在 ARM 中&#xff0c;通常为满减栈&#xff08;Full Descending FD&#xff09;, 也就是说&#xff0c;堆栈指针指向堆栈内存中最后一个填充的位置&#xff0c;并且随着每个新数据项被压入堆栈而递减。 栈的本质 要理解栈的本…

二叉搜索树、AVL树、红黑树底层源码以及迭代器模拟实现,map/set的封装

这次给大家分享的还是关于二叉树部分的内容&#xff0c;之前的文章已经分享过一些二叉树的基础知识&#xff0c;如果不了解的朋友可以看看&#xff1a;二叉树以及堆和堆排序。普通的二叉树其实是没有什么实际的应用价值的&#xff0c;而map和set大家用过或者听过吗&#xff1f;…

Metasploit Framework(MSF)对Metasploitable2的渗透解析

简介 Metasploitable2虚拟系统是一个特殊的ubuntu操作系统&#xff0c;本身设计目的是作为安全工具测试和演示常见漏洞攻击的环境。 其中最核心是可以用来作为MSF攻击用的靶机。这样方便我们学习MSF框架的使用。 并且开放了很多的高危端口如21、23、445等&#xff0c;而且具有…

李薇:大模型时代的数据变革

Datawhale干货 作者&#xff1a;李薇&#xff0c;上海人工智能实验室 前言 今天&#xff0c;我将向那些希望深入了解大模型的同学们&#xff0c;分享一些关于大模型时代的数据变革的知识。作为上海人工智能实验室OpenDataLab的产品主管&#xff0c;我会介绍我们在开放数据和大…

大数据技术闲侃之岗位选择解惑

前言 写下这篇文章是因为五一节前给群友的承诺&#xff0c;当然按照以往的惯例&#xff0c;也是我背后看到的这个现象&#xff0c;我发现大部分同学在投递岗位的时候都是投递数据分析岗位&#xff0c;其实背后并不是很清楚背后的岗位是做啥的&#xff0c;想想我自己的工作生涯…

用户/用户组管理

用户管理 * useradd 命令添加用户&#xff0c;会在/etc/passwd生成用户信息&#xff0c;信息分为7列&#xff0c;被6个冒号隔开 第一列 username (login name) 第二列 密码&#xff0c;但是该列已经被移除&#xff0c;用x表示&#xff0c;密码信息已经存放在了/etc/shadow文…

Android以aar包形式引入hunter-debug,Java(3)

Android以aar包形式引入hunter-debug&#xff0c;Java&#xff08;3&#xff09; &#xff08;1&#xff09;首先把hunter的master分支代码拉下来&#xff0c;在本地编译&#xff0c; https://github.com/Leaking/Hunterhttps://github.com/Leaking/Hunter此过程主要目的是获得…

理解学习曲线:芯片工作中的平台价值和个人价值

作为一名芯片工程师&#xff0c;从毕业出到步入公司的第一天开始&#xff0c;需要完成一次明显的转变&#xff0c;随着工作的日益开展和项目推进&#xff0c;个人能力的也得到了潜移默化的提升&#xff0c;当我们回看个人的知识/技能成长的曲线时&#xff0c;可能会发现很多的发…

CMake:递归检查并拷贝所有需要的DLL文件

文章目录 1. 目的2. 设计整体思路多层依赖的处理获取 DLL 所在目录探测剩余的 DLL 文件 3. 代码实现判断 stack 是否为空判断 stack 是否为空获取所有 target检测并拷贝 DLL 4. 使用 1. 目的 在基于 CMake 构建的 C/C 工程中&#xff0c;拷贝当前工程需要的每个DLL文件到 Visu…

将nacos从本地切换到远程服务器上时报错:客户端端未连接,Client not connected

报错信息&#xff1a; 09:34:38.438 [com.alibaba.nacos.client.Worker] ERROR com.alibaba.nacos.common.remote.client - Send request fail, request ConfigBatchListenRequest{headers{charsetUTF-8, Client-AppNameunknown, Client-RequestToken65c0fbf47282ae0a7b85178…

android点击事件,跳转界面

Android 事件处理 1&#xff0c;采用在Activity中创建一个内部类定义点击事件 主要xml代码 <?xml version"1.0" encoding"utf-8"?> <LinearLayout xmlns:android"http://schemas.android.com/apk/res/android"xmlns:app"http:…

【SAP Abap】X-DOC:SE37 - ABAP 功能模块之更新模块(Function Module 之 Update module)

【SAP Abap】X-DOC&#xff1a;SE37 - ABAP 功能模块之更新模块&#xff08;Function Module 之 Update module&#xff09; 1、简介1.1、什么是更新函数1.2、更新函数的类型1.3、更新函数的参数要求1.4、更新函数的调用方式1.5、更新函数的调试方式1.6、更新任务的执行模式1.7…

C语言——控制语句

目录 1. 分支语句1.1 if语句1.1.1 基本结构1.1.2 分层结构1.1.3 嵌套结构 1.2 switch case 语句 2.循环语句2.1 for循环2.1.1 基本结构2.1.2 嵌套结构2.1.3 变形 2.2 while循环2.3 do while循环2.4 死循环2.5 循环控制语句 控制语句即用来实现对程序流程的选择、循环、转向和返…

Shiro框架漏洞分析与复现

Shiro简介 Apache Shiro是一款开源安全框架&#xff0c;提供身份验证、授权、密码学和会话管理。Shiro框架直观、易用&#xff0c;同时也能提供健壮的安全性&#xff0c;可以快速轻松地保护任何应用程序——从最小的移动应用程序到最大的 Web 和企业应用程序。 1、Shiro反序列…

Linux下的线程(线程的同步与互斥)

目录 Linux下线程创建函数pthread_ create() 线程的等待函数pthread_ join() 线程终止 函数pthread exit() 函数pthread_cancel() 分离线程pthread_detach() 线程间的互斥 线程间同步 死锁 进程和线程 线程和进程是一对有意义的概念&#xff0c;主要区别和联系如下&am…