testsAndMisc/linux_configuration/C/atop_agg/test_atop_agg.c
Krzysztof kuhy Rudnicki 20d5d1f89b fix(usage_report): stop charging atop's HZ field as CPU; bundle since-last-report mode
atop's `-P PRC` output inserts the clock-tick rate (HZ=100) between the
`state` and `utime` columns. Both the Python parser and the native C
aggregator read that constant as utime for every record, charging a flat
1 CPU-second per record — so cpu_seconds collapsed to pid_count and
short-lived fork-storm commands (xset, dd, chronyc) topped the CPU table
(xset showed 67h). The old test fixtures lacked the HZ field, so code and
tests agreed on the bug.

- _parse_prc / atop_agg.c: read utime/stime past the HZ field (after+2/+3,
  tokens[10]/[11]); bump the length guards accordingly
- restore C/atop_agg (deleted in 89b4f59) under linux_configuration/C/,
  where the build path resolves; corrected test fixtures to include HZ
- _atop_agg_binary: fall back to the Python parser when the C source tree
  is gone instead of trusting an orphaned cached binary
- add regression tests proving HZ is not summed as CPU
- bundle the in-progress since-last-report multi-day aggregation (segments,
  -b/-e bounding, persisted state, window merging) and its tests/conftest
- meta: gate linux_configuration/tests in pytest_changed_packages.py

Verified by running usage_report.py --date 20260604: Top CPU now led by
SkyrimSE; xset/dd/chronyc fall to ~0. C unit tests + full pytest suite green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 18:13:47 +02:00

230 lines
7.3 KiB
C

/*
* Unit tests for atop_agg helpers. Compiled with --coverage; aims for
* 100% line coverage of atop_agg.c (excluding main, which is guarded
* by -DATOP_AGG_NO_MAIN).
*/
#include "atop_agg.h"
#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
static int failures = 0;
#define CHECK(cond) \
do \
{ \
if (!(cond)) \
{ \
fprintf(stderr, "FAIL %s:%d: %s\n", __FILE__, __LINE__, #cond); \
failures++; \
} \
} while (0)
static void test_copy_name(void)
{
char buf[16];
copy_name(buf, sizeof(buf), "(bash)");
CHECK(strcmp(buf, "bash") == 0);
copy_name(buf, sizeof(buf), "bash");
CHECK(strcmp(buf, "bash") == 0);
copy_name(buf, sizeof(buf), "()");
CHECK(strcmp(buf, "unknown") == 0);
copy_name(buf, sizeof(buf), "");
CHECK(strcmp(buf, "unknown") == 0);
/* Truncation. */
copy_name(buf, sizeof(buf), "(veryverylongnameabc)");
CHECK(strlen(buf) == sizeof(buf) - 1);
/* Fallback truncation: buf too small for "unknown" itself. */
char tiny[4];
copy_name(tiny, sizeof(tiny), "");
CHECK(strcmp(tiny, "unk") == 0);
}
static void test_tokenize(void)
{
char line[] = "PRC host 1000 2026/01/01 12:00:00 600 123 (bash) S 100 10 20\n";
char *toks[32];
int n = tokenize_line(line, toks, 32);
CHECK(n == 12);
CHECK(strcmp(toks[0], "PRC") == 0);
CHECK(strcmp(toks[7], "(bash)") == 0);
CHECK(strcmp(toks[9], "100") == 0); /* HZ field atop inserts before utime */
CHECK(strcmp(toks[10], "10") == 0); /* utime */
CHECK(strcmp(toks[11], "20") == 0); /* stime */
/* Multi-word parenthesised name. */
char line2[] = "PRM host 1000 d t 600 200 (Web Content) S 4096 1 2 0 0\n";
char *t2[32];
int n2 = tokenize_line(line2, t2, 32);
CHECK(n2 >= 12);
CHECK(strncmp(t2[7], "(Web Content)", 13) == 0);
/* Empty / whitespace-only line. */
char empty[] = " \n";
char *t3[4];
CHECK(tokenize_line(empty, t3, 4) == 0);
/* Max-tokens cap respected. */
char big[] = "a b c d e f g h i j k";
char *t4[3];
CHECK(tokenize_line(big, t4, 3) == 3);
/* Unclosed paren at EOL — consumed to end. */
char unclosed[] = "(abc";
char *t5[2];
int n5 = tokenize_line(unclosed, t5, 2);
CHECK(n5 == 1);
CHECK(strcmp(t5[0], "(abc") == 0);
}
static void test_process_and_emit(void)
{
State *s = state_new();
assert(s != NULL);
/* Two PRC samples for PID 100: first utime+stime=30, last=100.
Delta should be 70. The "100" after the state is atop's HZ field. */
char prc1[] = "PRC h 1000 d t 600 100 (cc1) S 100 10 20\n";
char prc2[] = "PRC h 1600 d t 600 100 (cc1) S 100 70 30\n";
process_line(prc1, s);
process_line(prc2, s);
/* One PRM sample for PID 100: rss=4096 kB. */
char prm1[] = "PRM h 1000 d t 600 100 (cc1) S 4096 100 4096 0 0\n";
process_line(prm1, s);
/* PRC sample for PID 200 seen only once → delta == last_ticks. */
char prc3[] = "PRC h 1000 d t 600 200 (short) S 100 5 5\n";
process_line(prc3, s);
/* Header / separator / unknown label should be ignored. */
char header[] = "# comment line\n";
process_line(header, s);
char sep[] = "SEP\n";
process_line(sep, s);
char other[] = "CPU h 1000 d t 600 0 0 0 0 0 0 0 0\n";
process_line(other, s);
/* Malformed: pid <= 0 (12 tokens so it passes the length guard and
actually reaches the pid check). */
char bad_pid[] = "PRC h 1000 d t 600 0 (x) S 100 1 1\n";
process_line(bad_pid, s);
/* PRC short (< 12 tokens) should hit the shared length guard, not crash. */
char prc_short[] = "PRC h 1000 d t 600 300 (y) S 1\n";
process_line(prc_short, s);
/* PRM short (< 12 tokens) hits the same shared length guard. */
char prm_short[] = "PRM h 1000 d t 600 300 (y) S 4096 1\n";
process_line(prm_short, s);
/* Emit and sanity-check the output. */
char *buf = NULL;
size_t sz = 0;
FILE *out = open_memstream(&buf, &sz);
assert(out != NULL);
emit_results(s, out);
fclose(out);
CHECK(strstr(buf, "W\t1000\t1600\t2\t600\n") != NULL);
/* cc1: cpu delta 70 (pid 100 two samples) + 0 pids column via max(cpu,ram).
Peak RSS 4096, sum_avg 4096, rss_samples 1, pids max(1,1)=1. */
CHECK(strstr(buf, "N\tcc1\t70\t4096\t4096\t1\t1\n") != NULL);
/* short: single-sample pid 200 → delta == 10; no RAM, so peak/sum/rss=0. */
CHECK(strstr(buf, "N\tshort\t10\t0\t0\t0\t1\n") != NULL);
free(buf);
state_free(s);
}
static void test_empty_and_single_epoch(void)
{
State *s = state_new();
/* No input at all → window line with zeroes. */
char *buf = NULL;
size_t sz = 0;
FILE *out = open_memstream(&buf, &sz);
emit_results(s, out);
fclose(out);
CHECK(strstr(buf, "W\t0\t0\t0\t0\n") != NULL);
free(buf);
state_free(s);
/* Exactly one epoch → median interval stays 0. */
s = state_new();
char prc[] = "PRC h 500 d t 600 50 (a) S 100 1 1\n";
process_line(prc, s);
buf = NULL;
sz = 0;
out = open_memstream(&buf, &sz);
emit_results(s, out);
fclose(out);
CHECK(strstr(buf, "W\t500\t500\t1\t0\n") != NULL);
free(buf);
state_free(s);
}
static void test_delta_clamped_to_zero(void)
{
/* Counter reset: last < first → delta must clamp to 0. */
State *s = state_new();
char a[] = "PRC h 100 d t 600 77 (x) S 100 50 50\n";
char b[] = "PRC h 700 d t 600 77 (x) S 100 10 10\n";
process_line(a, s);
process_line(b, s);
char *buf = NULL;
size_t sz = 0;
FILE *out = open_memstream(&buf, &sz);
emit_results(s, out);
fclose(out);
CHECK(strstr(buf, "N\tx\t0\t") != NULL);
free(buf);
state_free(s);
}
static void test_hash_collision(void)
{
/* Force two PIDs into adjacent slots (Knuth hash rarely collides on
small integers, but we sweep a range to exercise the linear-probe
branch). */
State *s = state_new();
for (int pid = 1; pid <= 2000; pid++)
{
char line[128];
snprintf(line, sizeof(line), "PRC h 1000 d t 600 %d (p) S 100 1 1\n", pid);
process_line(line, s);
snprintf(line, sizeof(line), "PRM h 1000 d t 600 %d (p) S 4096 1 1 0 0\n", pid);
process_line(line, s);
}
state_free(s);
}
static void test_state_free_null(void)
{
/* Freeing NULL must be safe. */
state_free(NULL);
}
int main(void)
{
test_copy_name();
test_tokenize();
test_process_and_emit();
test_empty_and_single_epoch();
test_delta_clamped_to_zero();
test_hash_collision();
test_state_free_null();
if (failures > 0)
{
fprintf(stderr, "%d test failures\n", failures);
return 1;
}
printf("atop_agg tests: OK\n");
return 0;
}