A C++ Config Validator for a Server That May Skip Your Job

# cpp# testing# devops# ai
A C++ Config Validator for a Server That May Skip Your JobFinley Zhou

A free server will not tell you when it skips your job. It will simply not run it. That is not a bug...

A free server will not tell you when it skips your job. It will simply not run it. That is not a bug in the platform; it is the contract of the tier. The only sane response is to design the job so a skipped run costs nothing.

This case study covers one small project end to end: a C++17 config validator, drafted with a free model, hardened with a spec table, and deployed as a nightly cron job on MonkeyCode's free server option. The spec table caught five defects the unit tests missed. The job survived skipped runs because it was designed to be disposable.

Background

A legacy build script read a hand-maintained INI file. Misspelled keys did not fail. They fell back to defaults, and the build produced an artifact nobody noticed was wrong.

The goal was a small validator that reads the file, checks it against a schema, and exits non-zero with sorted diagnostics. Constraints: C++17, one static binary, no third-party dependencies, and a hard runtime budget of one second per megabyte. The tool had to be boring. It also had to run somewhere free, because the point was to catch a slow, silent failure mode — not to justify a new paid service.

Implementation

Round 1: the draft

I generated the first draft with MonkeyCode's free model access. Disclosure: This article was prepared as part of MonkeyCode's product outreach.

The draft parsed sections, keys, and values, and it passed the unit tests I wrote while reading the code. That was the trap. Unit tests written from a model's code inherit the model's assumptions about the input format. The real file format had three details the draft got wrong, and none of my unit tests touched them.

Round 2: the spec table

I wrote a spec table from the actual file format, not from the code. Thirty-one cases in six categories:

# Category Cases What it guards
1 Section syntax 5 unterminated [, empty name, inline comment after ], whitespace, duplicate section
2 Key syntax 5 empty key, leading digit, illegal character, spaces around =, duplicate key
3 Values 6 quoted value, ; inside quotes, empty value, = inside value, trailing whitespace, unquoted ;
4 Comments 4 ; at line start, inline ;, ; after quoted value, comment-only line
5 Line endings 4 LF, CRLF, lone CR, blank lines
6 Schema 7 missing required key, wrong type, out-of-range value, unknown key, case sensitivity, default fallback, empty section

Round 1 failed five of the 31 cases: two CRLF cases, two quoted-semicolon cases, and one duplicate-section case. The unit tests still passed. Round 2 fixed the parsing but introduced a contract violation: the validator used exit code 1 for both "config invalid" and "internal error".

Round 3: the contract

The final parser core is unremarkable on purpose:

std::vector<Diagnostic> parse(std::istream& in, std::vector<Entry>& out) {
  std::vector<Diagnostic> diags;
  std::string section, raw;
  int line_no = 0;

  while (std::getline(in, raw)) {
    ++line_no;
    std::string line = trim(raw);              // CRLF -> LF
    if (line.empty() || line[0] == ';') continue;

    if (line[0] == '[') {
      auto close = line.find(']');
      if (close == std::string::npos) {
        diags.push_back({line_no, "unterminated section"});
        continue;
      }
      section = trim(line.substr(1, close - 1));
      if (section.empty()) diags.push_back({line_no, "empty section name"});
      continue;
    }

    auto eq = line.find('=');
    if (eq == std::string::npos) {
      diags.push_back({line_no, "expected '='"});
      continue;
    }

    std::string key = trim(line.substr(0, eq));
    std::string value = trim(line.substr(eq + 1));
    if (!is_valid_key(key)) {
      diags.push_back({line_no, "invalid key: " + key});
      continue;
    }

    // strip an inline comment only when it is outside quotes
    if (value.size() >= 2 && value.front() == '"' && value.back() == '"') {
      value = value.substr(1, value.size() - 2);
    } else if (auto c = value.find(';'); c != std::string::npos) {
      value = trim(value.substr(0, c));
    }

    out.push_back({section, key, value, line_no});
  }
  return diags;
}
Enter fullscreen mode Exit fullscreen mode

The interesting part is the exit-code contract, written into the repo before the code:

  • 0 — config is valid
  • 1 — config is invalid; diagnostics on stdout
  • 2 — the validator itself failed

Round 3 passed all 31 spec cases and honored the contract. Schema checks — required keys, types, ranges — live in a separate function that consumes the parsed entries, so the parser stays a pure syntax layer.

Deployment on a free server

The free server option was the right place for this job for one reason: the job is disposable. It runs nightly, holds no state, and a missed run costs nothing because the next run re-checks the whole file.

Four design rules made that true:

  1. One static binary. g++ -std=c++17 -O2 -static cfgcheck.cpp -o cfgcheck — no shared libraries, no runtime to install.
  2. No state. The tool reads a file and writes to stdout. No temp files, no lock files, no database.
  3. Bounded runtime. One megabyte in well under a second. The job cannot hang the server.
  4. Exit codes are the API. The cron wrapper branches on the documented codes, not on parsing the output.

The wrapper is deliberately boring:

#!/usr/bin/env bash
set -uo pipefail
BIN=/opt/cfgcheck/cfgcheck
CFG=/srv/build/config.ini
LOG=/var/log/cfgcheck.log

"$BIN" "$CFG" >>"$LOG" 2>&1
code=$?

if [[ $code -eq 1 ]]; then
  echo "[$(date -u +%FT%TZ)] config invalid:" >>"$LOG"
  "$BIN" "$CFG" 2>&1 | head -20 >>"$LOG"
fi

exit 0
Enter fullscreen mode Exit fullscreen mode

The wrapper exits 0 on purpose. The log is the artifact, not the exit code. A second cron line, five minutes later, checks the log's mtime and fails if the job never ran:

# crontab
0 3 * * * /opt/cfgcheck/nightly-cfgcheck.sh
5 3 * * * test -n "$(find /var/log/cfgcheck.log -mmin -60)" || echo "cfgcheck missed its run" >>/var/log/cfgcheck.missed
Enter fullscreen mode Exit fullscreen mode

That separation matters. The validator's exit code describes the config file; the freshness check describes the infrastructure. Mixing the two is how a skipped job becomes a silent one.

Decision table

I used this table to decide the free server was acceptable:

Property Free server is fine Free server is not enough
State stateless, idempotent stateful sessions, queues
Missed run rerun later, no harm data loss, missed deadline
Latency minutes acceptable user-facing request path
Alerting log file + mtime check must page someone in seconds
Secrets none API keys, customer data

Results

The spec table caught three defect classes the unit tests never touched:

  1. CRLF values. std::getline keeps the \r. The first draft compared "true" against "true\r" and reported a schema violation on every Windows-edited file.
  2. Semicolons inside quotes. url = "https://x/a;b" — the first draft stripped everything after ;, producing a truncated URL that still looked plausible.
  3. Duplicate sections. The first draft merged [build] blocks silently. The spec table required an error.
Round Unit tests Spec table Exit-code contract
1 pass 5 failures missing
2 pass 2 failures wrong codes
3 pass 0 failures correct

Runtime on a 1MB file stayed under 20ms in my environment; the code above is complete enough to reproduce that measurement. The deployment ran without a false alarm. The freshness check is what makes a skipped run visible instead of silent.

Lessons

The spec table is the contract; the model is a fast typist. Unit tests written from the model's code inherit the model's assumptions. The spec table comes from the file format — from the real world — and that is where its power comes from.

Free infrastructure rewards disposable jobs. If a skipped run is harmless, a free server is enough. The design work is not in the code; it is in making the job stateless, idempotent, and bounded.

Exit codes are the API. The model's first version printed "ERROR" and returned 0. The human invariant — 0, 1, 2, documented in the repo — is what the cron wrapper depends on. Without it, the wrapper is guessing.

Limitations

This approach is not for everyone. The spec table is not a fuzzer; the config file is trusted input here, and if your file comes from users, add a fuzzing gate before relying on this validator. If a missed run is unacceptable, or if the job must alert someone within minutes, a free server is the wrong tool. And the numbers above are specific to this file format — copy the method, not the cases.

If you are about to paste a free model's draft into CI, write the spec table first. It takes an hour, and it will find the defects your unit tests were written to miss.