Back to Repositories

Testing Process Logging Implementation in OpenPilot

This test suite validates the process logging functionality in the OpenPilot system, focusing on parsing and monitoring system processes, CPU utilization, and memory information. The tests ensure accurate system metrics collection and process state tracking.

Test Coverage Overview

The test suite provides comprehensive coverage of process logging components:

Key areas tested include:
  • Process stat parsing and validation
  • CPU time metrics collection
  • Memory information gathering
  • Command line argument parsing
  • Process logger message building
Edge cases covered include handling special characters in process names and empty command line arguments.

Implementation Analysis

The testing approach utilizes Catch2 framework for C++ unit testing, implementing both string-based parsing tests and system integration tests. The suite employs section-based test organization with both mock data validation and actual system calls verification.

Key patterns include:
  • Mock data injection for controlled testing
  • System integration validation
  • Comprehensive state verification

Technical Details

Testing tools and configuration:
  • Catch2 testing framework
  • Linux proc filesystem integration
  • Custom Parser class implementation
  • MessageBuilder for data serialization
  • Cereal serialization framework
  • System process monitoring utilities

Best Practices Demonstrated

The test suite exemplifies several testing best practices:

  • Modular test organization using SECTION blocks
  • Comprehensive error checking and validation
  • Both unit and integration test coverage
  • Effective mock data usage
  • Clear test case separation
  • Robust error handling verification

commaai/openpilot

system/proclogd/tests/test_proclog.cc

            
#define CATCH_CONFIG_MAIN
#include "catch2/catch.hpp"
#include "common/util.h"
#include "system/proclogd/proclog.h"

const std::string allowed_states = "RSDTZtWXxKWPI";

TEST_CASE("Parser::procStat") {
  SECTION("from string") {
    const std::string stat_str =
        "33012 (code )) S 32978 6620 6620 0 -1 4194368 2042377 0 144 0 24510 11627 0 "
        "0 20 0 39 0 53077 830029824 62214 18446744073709551615 94257242783744 94257366235808 "
        "140735738643248 0 0 0 0 4098 1073808632 0 0 0 17 2 0 0 2 0 0 94257370858656 94257371248232 "
        "94257404952576 140735738648768 140735738648823 140735738648823 140735738650595 0";
    auto stat = Parser::procStat(stat_str);
    REQUIRE(stat);
    REQUIRE(stat->pid == 33012);
    REQUIRE(stat->name == "code )");
    REQUIRE(stat->state == 'S');
    REQUIRE(stat->ppid == 32978);
    REQUIRE(stat->utime == 24510);
    REQUIRE(stat->stime == 11627);
    REQUIRE(stat->cutime == 0);
    REQUIRE(stat->cstime == 0);
    REQUIRE(stat->priority == 20);
    REQUIRE(stat->nice == 0);
    REQUIRE(stat->num_threads == 39);
    REQUIRE(stat->starttime == 53077);
    REQUIRE(stat->vms == 830029824);
    REQUIRE(stat->rss == 62214);
    REQUIRE(stat->processor == 2);
  }
  SECTION("all processes") {
    std::vector<int> pids = Parser::pids();
    REQUIRE(pids.size() > 1);
    for (int pid : pids) {
      std::string stat_path = "/proc/" + std::to_string(pid) + "/stat";
      INFO(stat_path);
      if (auto stat = Parser::procStat(util::read_file(stat_path))) {
        REQUIRE(stat->pid == pid);
        REQUIRE(allowed_states.find(stat->state) != std::string::npos);
      } else {
        REQUIRE(util::file_exists(stat_path) == false);
      }
    }
  }
}

TEST_CASE("Parser::cpuTimes") {
  SECTION("from string") {
    std::string stat =
        "cpu  0 0 0 0 0 0 0 0 0 0
"
        "cpu0 1 2 3 4 5 6 7 8 9 10
"
        "cpu1 1 2 3 4 5 6 7 8 9 10
";
    std::istringstream stream(stat);
    auto stats = Parser::cpuTimes(stream);
    REQUIRE(stats.size() == 2);
    for (int i = 0; i < stats.size(); ++i) {
      REQUIRE(stats[i].id == i);
      REQUIRE(stats[i].utime == 1);
      REQUIRE(stats[i].ntime ==2);
      REQUIRE(stats[i].stime == 3);
      REQUIRE(stats[i].itime == 4);
      REQUIRE(stats[i].iowtime == 5);
      REQUIRE(stats[i].irqtime == 6);
      REQUIRE(stats[i].sirqtime == 7);
    }
  }
  SECTION("all cpus") {
    std::istringstream stream(util::read_file("/proc/stat"));
    auto stats = Parser::cpuTimes(stream);
    REQUIRE(stats.size() == sysconf(_SC_NPROCESSORS_ONLN));
    for (int i = 0; i < stats.size(); ++i) {
      REQUIRE(stats[i].id == i);
    }
  }
}

TEST_CASE("Parser::memInfo") {
  SECTION("from string") {
    std::istringstream stream("MemTotal:    1024 kb
MemFree:    2048 kb
");
    auto meminfo = Parser::memInfo(stream);
    REQUIRE(meminfo["MemTotal:"] == 1024 * 1024);
    REQUIRE(meminfo["MemFree:"] == 2048 * 1024);
  }
  SECTION("from /proc/meminfo") {
    std::string require_keys[] = {"MemTotal:", "MemFree:", "MemAvailable:", "Buffers:", "Cached:", "Active:", "Inactive:", "Shmem:"};
    std::istringstream stream(util::read_file("/proc/meminfo"));
    auto meminfo = Parser::memInfo(stream);
    for (auto &key : require_keys) {
      REQUIRE(meminfo.find(key) != meminfo.end());
      REQUIRE(meminfo[key] > 0);
    }
  }
}

void test_cmdline(std::string cmdline, const std::vector<std::string> requires) {
  std::stringstream ss;
  ss.write(&cmdline[0], cmdline.size());
  auto cmds = Parser::cmdline(ss);
  REQUIRE(cmds.size() == requires.size());
  for (int i = 0; i < requires.size(); ++i) {
    REQUIRE(cmds[i] == requires[i]);
  }
}
TEST_CASE("Parser::cmdline") {
  test_cmdline(std::string("a\0b\0c\0", 7), {"a", "b", "c"});
  test_cmdline(std::string("a\0\0c\0", 6), {"a", "c"});
  test_cmdline(std::string("a\0b\0c\0\0\0", 9), {"a", "b", "c"});
}

TEST_CASE("buildProcLoggerMessage") {
  MessageBuilder msg;
  buildProcLogMessage(msg);

  kj::Array<capnp::word> buf = capnp::messageToFlatArray(msg);
  capnp::FlatArrayMessageReader reader(buf);
  auto log = reader.getRoot<cereal::Event>().getProcLog();
  REQUIRE(log.totalSize().wordCount > 0);

  // test cereal::ProcLog::CPUTimes
  auto cpu_times = log.getCpuTimes();
  REQUIRE(cpu_times.size() == sysconf(_SC_NPROCESSORS_ONLN));
  REQUIRE(cpu_times[cpu_times.size() - 1].getCpuNum() == cpu_times.size() - 1);

  // test cereal::ProcLog::Mem
  auto mem = log.getMem();
  REQUIRE(mem.getTotal() > 0);
  REQUIRE(mem.getShared() > 0);

  // test cereal::ProcLog::Process
  auto procs = log.getProcs();
  for (auto p : procs) {
    REQUIRE(allowed_states.find(p.getState()) != std::string::npos);
    if (p.getPid() == ::getpid()) {
      REQUIRE(p.getName() == "test_proclog");
      REQUIRE(p.getState() == 'R');
      REQUIRE_THAT(p.getExe().cStr(), Catch::Matchers::Contains("test_proclog"));
      REQUIRE_THAT(p.getCmdline()[0], Catch::Matchers::Contains("test_proclog"));
    }
  }
}