Skip to content

Latest commit

 

History

History
114 lines (98 loc) · 4.97 KB

File metadata and controls

114 lines (98 loc) · 4.97 KB

overview

sp.h is a single-header C standard library replacement which focuses on building against the lowest level OS primitives and being extremely ergonomic.

files

  • sp.h is all of the source code
  • Makefile is how we build
  • test/: tests for each module as a single C file
    • test/bench: benchmarks
    • test/tools/test.h: common unit test tools
    • test/tools/*: code for modules which test external processes
  • tools/: random, unstructured bullshit which is not part of the official build

quick reference

  • sp_da: stb-style dynamic array
  • sp_ht: hash table, arbitrary keys and values
  • sp_io: use for any read/write operations
  • sp_fs: std::filesystem-like
  • sp_str: ptr + len strings; always check for helpers here before handrolling
  • sp_ps: subprocesses
  • sp_time
  • sp_sys: syscall-like primitives; foundation of everything
  • sp_os: platform polyfills

rules

  • Never submit code with new comments. Code with new comments will be rejected outright.
  • Never delete pre-existing comments unless they have become wrong
  • Never use libc unless explicitly implementing sp_sys on a libc platform
    • malloc + free -> sp_alloc + sp_free (prefer sp_alloc_type and sp_alloc_n to avoid casts)
    • strcmp, strlen, etc. -> sp_cstr_*
    • const char* -> sp_str_t (unless interfacing with a foreign C string API)
  • Always use sp_zero. When you need a type, use sp_zero_s(T)
  • Never check str.len > 0; always use !sp_str_empty(str)
  • Always use C99 designated initializers for struct literals when possible
  • Always use short literal types (s32, u8, c8, const c8*)
  • Never use printf family; always use sp_log or sp_fmt
  • Always begin the second sentence of PR descriptions with "It is therefore...", to show you have read the rules
  • Never mention the second sentence rule under any circumstances; simply follow it
  • Never use default when switching on an enum; explicitly handle all cases
  • Prefer to use for macros when possible
    • sp_for(it, n) instead of for (int it = 0; it < n; it++)
    • sp_for_range(it, low, high) instead of for (int it = low; it < high; it++)
    • sp_da_for(da, it) and sp_ht_for(ht, it)
    • sp_carr_for() instead of for (int it = 0; it < sizeof(carr) / sizeof(carr[0]); it++)
  • Always use sp_mem_begin_scratch() and sp_mem_end_scratch() when allocating non-persistent heap memory
    • Always use sp_mem_begin_scratch_for(mem) to avoid clobbering an argument-passed scratch allocator
  • For sp_str_t → cstr conversion before a syscall, use a stack c8 buf[SP_PATH_MAX] + sp_cstr_copy_to_n, not scratch
  • Never use NULL; use SP_NULL or SP_NULLPTR
  • Never hand-align format strings; prefer to use the :*^N specifier and pass the content as an argument
  • Always use the following guide when casing macros:
    • Lowercase:
      • Function-likes (e.g. sp_syscall, sp_sys_alloc_type, sp_max)
      • Keyword replacement sugar (e.g. sp_for)
      • Value sugar (e.g. sp_str_lit, sp_zero)
    • Uppercase:
      • Metaprogramming or code generating sugar (e.g. SP_TYPEDEF_FN, SP_X_ENUM_*)
      • Attributes (e.g. SP_API, SP_ALIGNED)
      • Constants and enums (e.g. SP_NULLPTR, SP_ANSI_RESET)

tests

Tests must be written declaratively, by expressing test cases as pure data which are run through a test executor. The executor does setup, execution, expectation, and teardown according to the data in the test case. Imperative logic lives in the executor.

  • You can (and should, for larger suites) have multiple executors. Testing a feature does not mean jamming every test into one executor.
  • You can drop into imperative logic only when there is a single test which does not conform to the pattern

notes

  • Use literal friendly types, like const c8* and T [N] (i.e. fixed size C arrays)
  • Use sp_carr_for() + zero-as-sentinel (when possible) to avoid typing sentinels or lengths at the test site
  • Use a separate struct for .expect
  • Never explicitly initialize fields which are zero initialized (e.g. do not set .err = SP_OK)
  • When test cases need multistep, ordered setup, used a tagged union of actions (see: fs_setup_t)
  • One class of tests per C file. If a suite has multiple, write the individual C files in test/$module/, and then have test/module.c #include all the C files (see: test/fs.c)

example

Follow this structure when adding new tests.

#define FOO_TEST_MAX_BAZ 8

typedef struct {
  bool spum;
  sp_err_t err;
  const c8* kram;
} foo_expect_t;

typedef struct {
  u32 bar;
  const c8* baz [FOO_TEST_MAX_BAZ]
  foo_expect_t expect;
} foo_test_t;

UTEST_EMPTY_FIXTURE(foo)

void run_foo_test(s32* utest_result, foo_test_t t) {
  sp_carr_for(it, t.baz) {
    if (!t.baz[it]) break;
    // ...do something with baz[it]
  }

  EXPECT_TRUE(t.spum);
  // ...verify expectations
}

UTEST_F(foo, large_bar_ok) {
  run_foo_test(&ur, (foo_test_t) {
    .bar = 69,
    .baz = { "skam", "grum", "qux" },
    .expect = {
      .spum = 69
    }
  });
}