23 Commits

Author SHA1 Message Date
51144038ce Point AGENTS.md at the issue tracker for outstanding work
Some checks are pending
libakstdlib CI Build / cmake_build (push) Waiting to run
libakstdlib CI Build / sanitizers (push) Waiting to run
libakstdlib CI Build / coverage (push) Waiting to run
libakstdlib CI Build / mutation_test (push) Waiting to run
The agent instructions now say where new work goes -- an issue on the forge,
labelled by kind and blast radius and carrying status::grooming until its scope
is settled -- and that TODO.md is the record of where the library stands, what
is deliberately not wrapped, and which uncovered lines are uncoverable rather
than untested.

Also: a defect in a dependency is filed against that dependency. Three defects
in libakerror that cost this library a workaround each sat in this repository's
own notes without anybody upstream being able to see them, which is how a
workaround gets written once per consumer.

The KNOWN_FAILING_TESTS and pull-request rules now name the issue rather than a
TODO entry.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02 19:24:45 -04:00
91a8896b18 Correct the error-pool entry: the pin is 1.0.0, upstream locks it
Some checks failed
libakstdlib CI Build / sanitizers (push) Has been cancelled
libakstdlib CI Build / coverage (push) Has been cancelled
libakstdlib CI Build / mutation_test (push) Has been cancelled
libakstdlib CI Build / cmake_build (push) Has been cancelled
The largest item in the file said the error pool has no synchronisation of any
kind and that libakstdlib is therefore permanently single-threaded pending a
decision upstream. That is true of deps/libakerror as pinned -- 1.0.0, zero
occurrences of akerr_mutex_lock, no lock.h, no thread tests -- and false of
libakerror 2.0.1, which holds one recursive lock over the pool and the status
registry and ships docs/thread-safety.md.

libakgl and akbasic both pin 2.0.1 already.

Issue #2 is retitled and rescoped to the submodule bump and the concurrent test
that has to prove it, rather than a wait on somebody else.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02 18:57:39 -04:00
dbe1ccaf57 Move outstanding work from TODO.md into the issue tracker
Some checks failed
libakstdlib CI Build / cmake_build (push) Has been cancelled
libakstdlib CI Build / sanitizers (push) Has been cancelled
libakstdlib CI Build / coverage (push) Has been cancelled
libakstdlib CI Build / mutation_test (push) Has been cancelled
Twenty-six issues on source.starfort.tech/andrew/libakstdlib, labelled by kind
and blast radius and milestoned by what they can land in: 0.2.x for anything
that breaks no ABI, 0.3.0 for new public symbols, 1.0.0 for the large surfaces.
Everything filed carries status::grooming.

The four items blocked on libakerror are filed in both places -- the fix is
there and the bill is here -- and TODO.md keeps a table saying which is which.

What stays in TODO.md is what a tracker has no place for: where the library
stands, why six libc functions are deliberately not wrapped, which eight
uncovered lines are uncoverable rather than untested, and the akbasic call
counts that prioritised everything built since. 377 lines to 189.

Filed alongside: 25 file headers in tests/ and src/ cite TODO.md section
numbers from a numbering the file itself had already abandoned. They label
completed work, so nothing about the code is misleading -- only the pointer is.
Left for its own commit rather than churning 25 files here.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02 18:55:34 -04:00
669b2b395f Record that there is no directory-reading wrapper
All checks were successful
libakstdlib CI Build / cmake_build (push) Successful in 2m55s
libakstdlib CI Build / sanitizers (push) Successful in 2m52s
libakstdlib CI Build / coverage (push) Successful in 2m43s
libakstdlib CI Build / mutation_test (push) Successful in 12m30s
opendir/readdir/closedir have no aksl_* counterpart, so a consumer that
wants to list a directory has to call them itself and report through errno
in a file where everything else reports through an akerr_ErrorContext *.

Found from akbasic, which needs it for Commodore BASIC's DIRECTORY verb and
refuses the verb rather than working around the gap.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 21:50:07 -04:00
f8425b8729 Gate mutation testing on a measured score, not an inherited one
All checks were successful
libakstdlib CI Build / cmake_build (push) Successful in 2m53s
libakstdlib CI Build / sanitizers (push) Successful in 2m51s
libakstdlib CI Build / coverage (push) Successful in 2m43s
libakstdlib CI Build / mutation_test (push) Successful in 12m37s
The threshold had been 80 against src/stdlib.c alone. There are three
more sources now and nobody had measured them, so that number was a
guess carried forward.

Measured: 72.3%, 188 of a 260-mutant sample from the 1701 the four
sources generate. Gate set to 65 -- a ratchet with headroom for the
runner and for the sample shifting as sources change, not a target.

A sample rather than the whole set, because 1701 rebuilds and test runs
is hours. --max-mutants samples by even index rather than at random, so
the same 260 run every time and the gate stays reproducible; sampling
all four files beats exhausting one of them, which is what this job did
before.

72.3% against the 89.6% reported at 0.1.0 is a change in denominator,
not a regression in the tests. That figure covered one 561-line file;
this covers four totalling 1716 lines, and most of the added surface is
argument validation whose mutants are frequently *equivalent* -- 12 of
the 72 survivors are `errno = 0` deleted from a wrapper whose libc call
always sets errno, which no test that could be written would catch. The
README breaks all 72 down and says which are worth acting on; TODO.md
2.4 carries the three clusters that are.

Two of them were real and are fixed here and in the previous commit: the
right child's `depth + 1` in the depth-first walk, and aksl_tree_remove
on an empty tree, which without its guard dereferences NULL. Neither had
a test; both do now. That is what the harness is for.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 08:15:08 -04:00
cc5e7899bb Cap the depth of a right-leaning tree too
Mutation testing found it: changing the right child's `depth + 1` to
`depth + 0` in the depth-first recursion survived the entire suite. The
depth cap was only ever tested against a chain that leans left, so
nothing said whether the right-hand descent counted at all -- a tree
that goes right for a million nodes would have recursed until the
process died, which is precisely what AKSL_TREE_MAX_DEPTH exists to
prevent.

The right-leaning chain is now tested in all three depth-first orders
and breadth-first, which carries its depth on the queue entry instead.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 08:09:04 -04:00
3ad3994762 Test aksl_vscanf, and drop a check that could not fail
Coverage caught both. aksl_vscanf was declared, documented and never
called by anything, which is what 100% function coverage is for -- it is
the only metric here that notices a whole function nobody exercises.

aksl_vasprintf had a second vsnprintf whose return it compared against
the first, and a CLEANUP block to free the buffer when they disagreed.
They cannot disagree: the buffer was sized by vsnprintf from the same
format string and the same arguments a few lines earlier. So that was a
failure branch nothing can reach and a free() beneath it that nothing
can execute -- exactly the dead code TODO.md 2.2.11 recorded against the
old aksl_memset and aksl_memcpy, and removing those was the point. The
invariant is a comment now, where it can be read.

Back to 99.5% of lines (1708/1716) and 100% of functions (154/154), with
the eight uncovered lines the ones TODO.md already accounts for.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 08:07:25 -04:00
7940276f87 Test the installed package, not just the build tree
Found by building a consumer against a fresh install: an installed
libakstdlib was not usable. akstdlibConfig.cmake calls
find_dependency(akerror), and the top-level build pulls the submodule in
EXCLUDE_FROM_ALL, so `cmake --install` on this project installs only
this project and leaves that dependency unresolvable.

CI had been hiding it by installing libakerror@main in a separate step
-- a different libakerror from the one actually compiled, which is the
inconsistency TODO.md 2.3 recorded and the previous commit removed.
Removing it exposed the real gap.

CI now installs deps/libakerror, the same commit the top-level build
compiles, so there is one libakerror in play and the install is
consumable.

tests/consumer/ is the check that would have caught it: a standalone
project, configured against CMAKE_PREFIX_PATH rather than as part of
this build, because being part of this build is exactly what would let
it pass without testing anything. It exercises what only an install has
-- find_package with a version request against the generated version
file, find_dependency(akerror) resolving, and the exported
akstdlib::akstdlib target -- and touches one function from each of the
four sources, so a library installed with a source file missing from its
link line fails there rather than in the next consumer to find it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 08:05:09 -04:00
b104a07eb4 Reindent to the canonical stroustrup style
Whitespace only -- `git diff -w` is empty. TODO.md 2.3 recorded that the
tree mixed hard tabs and space indents within the same functions;
tests/aksl_capture.h was the worst of it, with the assertion macros
space-indented and everything around them tabbed.

The style is the one libakerror standardised on and the one AGENTS.md
already describes: Emacs cc-mode "stroustrup", c-basic-offset 4,
indent-tabs-mode on, tab-width 8. A correct file is a fixed point of
indent-region under those settings, and the tree is one now -- a second
pass produces no diff.

scripts/reindent.el is that pass, checked in so "correct" is something
you can run rather than something you have to remember. It is not
clang-format and deliberately so: AGENTS.md forbids introducing one
without discussion, and this is a good illustration of why. cc-mode
indents every declaration in akstdlib.h one level for the extern "C" { }
wrapping the file body, so the script sets inextern-lang to 0 -- a
cc-mode offset with no clang-format equivalent, and without it the first
run moved 364 lines in the wrong direction.

Its own commit, no behaviour change, and the suite is green either side.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 08:02:55 -04:00
125eeb2109 Version at 0.2.0: complete the wishlist, document it, gate the docs
Closes what was left of TODO.md sections 1, 2 and 3, and rewrites that
file to hold outstanding items only.

The API break gets a minor bump, because pre-1.0 the soname carries
MAJOR.MINOR and 0.1 and 0.2 are therefore different ABIs. Five
signatures changed and the ato* contract with them; UPGRADING.md is new
and lists every one, with the before/after for the cases the compiler
cannot warn about.

Section 3.1 is finished: reallocarray with the multiplication checked,
aligned_alloc and posix_memalign, asprintf/vasprintf, scanf/vscanf.
Four functions on that list are deliberately absent rather than missing
-- sprintf, strtok, setbuf and perror -- and TODO.md now says which and
why, so nobody adds them thinking they were forgotten.

Section 1.9, the cross-cutting tests:

  tests/test_pool.c    drives every failure path AKERR_MAX_ARRAY_ERROR
                       + 10 times and checks the pool after each round,
                       because a wrapper that leaks a slot fails a
                       hundred calls later in unrelated code. It also
                       asserts that each error names the function and
                       file it was raised from, which is what catches a
                       FAIL that migrates into a helper during a
                       refactor: status right, message right, origin
                       quietly lying.
  tests/negative/      two sources that must FAIL to compile, built with
                       -Werror and registered WILL_FAIL. AKERR_NOIGNORE
                       and the format attributes are enforced by the
                       compiler and by nothing else; drop either and
                       every ordinary test still passes.

Thread safety is answered rather than tested: the library is not
thread-safe and cannot be made so from here, because libakerror's error
pool is an unlocked process-global array. README.md says so plainly and
TODO.md carries it as the item blocking any future pthread wrappers.

Doxygen is configured and gated. All 147 public functions have @brief,
a @param each, @throws per status and @return; EXTRACT_ALL is off and
WARN_NO_PARAMDOC on, so `cmake --build build --target docs` fails on an
undocumented entity. It ran to 0 warnings. The Doxyfile carries no
version -- cmake/RunDoxygen.cmake feeds PROJECT_NUMBER in from
project(), so that stays the one place a version is written.

CI now builds against the submodule it pins instead of also installing
libakerror@main and never linking it, adds -Werror, and gains a
sanitizer job. The pre-push hook matches, and runs the docs check too.

Coverage: 99.5% of lines (1643/1651), 100% of functions (147/147). The
eight uncovered lines are each uncovered on purpose and TODO.md says
which and why.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 08:00:16 -04:00
98a0a8562d Wrap the strings, streams and collections the wishlist asked for
TODO.md section 3.1's high-priority list and section 3.6's data
structures. This is the surface akbasic went without: it makes 10 calls
into this library and 116 to raw libc, and 69 of those 116 are strlen,
strcmp, strncpy and strstr.

Three new translation units, because src/stdlib.c covering four times
what it did would stop being readable:

  src/string.c       lengths, bounded copy and concatenation, duplication,
                     comparison including the case-insensitive forms,
                     searching, the reentrant tokenisers, and a status
                     message that knows this library's own statuses as
                     well as errno's.
  src/stream.c       positioning, flushing and buffering, character and
                     line I/O, stream state, freopen/fdopen/tmpfile,
                     formatted input, and the file operations.
  src/collections.c  the list functions that were missing, a head/tail
                     container so append is O(1), a binary search tree,
                     FNV-1a, a fixed-capacity hash map and a growable
                     string buffer.

Two conventions run through all of it. The copying functions take the
destination size even where the libc function they are named for does
not, because strcpy(3) cannot be called safely without it, and
truncation is an error that writes nothing rather than a plausible
prefix -- this is the idiom akbasic writes out by hand at ten sites.
The searching functions treat "not found" as a successful answer of
NULL, because absent is an answer and raising on it would make every
caller handle a non-error.

The hash map is akbasic's src/symtab.c generalised: open-addressed with
linear probing over a caller-supplied slot array, keys copied into fixed
slots so the map owns them, tombstones on delete so a removal cannot cut
a probe chain, and a refusal rather than a resize when full.

Tests: 16 binaries, green under the normal and sanitizer builds. The
scanf wrappers take the number of conversions the caller expects, since
comparing scanf(3)'s return against that by hand is the check everyone
eventually forgets.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 07:32:35 -04:00
55eb0334c4 Fix the six confirmed defects and close the API contract gaps
TODO.md section 2.1 recorded six defects reproduced against the built
library, and section 2.2 seventeen contract gaps. Both are closed. The
four tests registered in AKSL_KNOWN_FAILING_TESTS are folded back into
the tests for the things they test, and that list is now empty.

The defects:

  2.1.1  aksl_list_append conflated Floyd cycle detection with finding
         the tail, so `tail` tracked the node behind the midpoint. Any
         append to a list of 2+ nodes silently dropped everything after
         it. Two separate walks now: Floyd to prove the list is finite,
         then a plain walk to the end.
  2.1.2  aksl_list_iterate started visiting from Floyd's `slow` cursor,
         so the whole first half of the list -- head included -- was
         never passed to the callback. It starts at the head.
  2.1.3  AKERR_ITERATOR_BREAK did not stop a tree traversal: the frame
         that raised it handled it and returned success, so the parent
         carried on into the sibling subtree. The recursion is split out
         and propagates the break; only the public entry swallows it.
  2.1.4  va_end now matches every va_start on every path.
  2.1.5  The ato* family had no error channel at all. Reimplemented over
         a new strto* family with errno cleared, an endptr check and a
         range check: AKERR_VALUE for junk, ERANGE for overflow.
  2.1.6  aksl_realpath never checked resolved_path, could not be told
         the buffer size, and formatted an unspecified buffer with %s on
         its own error path. It takes a length; aksl_realpath_alloc is
         the allocating form.

The contract gaps, in brief: errno is cleared before every wrapped call
and read back through a fallback so no error can carry status 0; fopen
validates pathname and mode; fread/fwrite report the transferred count
through a required out-param and no longer call a short transfer a
success; aksl_sprintf is gone in favour of aksl_snprintf, which treats
truncation as an error; the variadic wrappers carry format attributes;
djb2 reads bytes as unsigned; tree traversal is depth- and cycle-bounded
and implements BFS, so lalloc/lfree are used rather than merely stored;
an unknown searchmode is AKERR_VALUE rather than silent success;
list_pop takes the head by reference; aksl_freep, the node initialisers
and extern "C" are new.

Build: -pg is out of the default build (it never reached the C compiler
anyway, and it is what produced the stray gmon.out), -Wall -Wextra are
in, and there is a .gitignore.

Tests: 11 binaries, all green under the normal and sanitizer builds.
Visit-order assertions replace the step counts that could not tell the
three depth-first orders apart.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 07:14:11 -04:00
fd71bcc67b Record what the first full consumer had to work around
All checks were successful
libakstdlib CI Build / cmake_build (push) Successful in 2m48s
libakstdlib CI Build / coverage (push) Successful in 2m37s
libakstdlib CI Build / mutation_test (push) Successful in 10m9s
akbasic is a ~6,300-line C interpreter built on this library. It is the first
consumer to exercise the whole surface rather than a corner of it, so what it
could not use is worth writing down: it prioritizes the section 3 wishlist by
what a real port actually reaches for, and it says which section 2 entries have
teeth.

The number that matters: across src/, akbasic makes 10 calls into this library
and 116 to raw libc. A library whose value proposition is turning silent libc
failures into error contexts is being bypassed 92% of the time by the consumer
most committed to it.

Four things it had to write for itself, each of which maps onto an existing
unchecked box. A strict strtoll/strtod wrapper, because 2.1.5 means aksl_atoi
would have turned four diagnosable errors into wrong answers -- VAL("garbage")
answering 0.0 instead of raising. A fixed-capacity string-keyed hash table,
which is 3.6's "hash map built on aksl_strhash_djb2", needed three times over
for variables, functions and labels. The bounded-copy-with-truncation-as-error
idiom at ten sites across six files. And case folding at three sites.

Also confirms 2.2.4 (aksl_sprintf unbounded -- akbasic deliberately never calls
it, and has 28 snprintf sites with no aksl_snprintf to route them through),
2.2.2 (aksl_fopen validation, reached from user input via DLOAD/DSAVE), 2.2.6
(the djb2 sign extension, benign here only because BASIC identifiers are ASCII)
and 2.1.4 (the missing va_end, which akbasic's text sink runs on every line of
program output).

Section 4.3 records what akbasic did *not* need, so the wishlist does not get
reordered purely by one consumer's shape: it allocates nothing and uses no lists
or trees, so a consumer that does allocate would weight 3.1's memory section far
higher.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 06:29:01 -04:00
95e5002512 Version the library at 0.1.0
All checks were successful
libakstdlib CI Build / cmake_build (push) Successful in 2m49s
libakstdlib CI Build / coverage (push) Successful in 2m37s
libakstdlib CI Build / mutation_test (push) Successful in 10m13s
project() now carries VERSION 0.1.0, and is the single place a version number
is spelled. It flows into generated version macros, the shared library's
VERSION/SOVERSION, the Version: field in akstdlib.pc, and a new
akstdlibConfigVersion.cmake. Before this @PROJECT_VERSION@ expanded to
nothing, so akstdlib.pc shipped an empty Version: and libakstdlib.so carried
no soname at all.

0.x on purpose: TODO.md section 2.1 still records four confirmed defects whose
fixes change documented behaviour, so the API is not being promised yet. While
the major version is 0 the soname carries MAJOR.MINOR -- 0.1 and 0.2 are
different ABIs -- and becomes MAJOR alone at 1.0. The if() in CMakeLists.txt
and the #if in tests/test_version.c encode that rule and are tested against
each other.

include/akstdlib_version.h.in is configured into the build tree as
akstdlib_version.h and installed beside akstdlib.h. It defines
AKSL_VERSION_MAJOR/MINOR/PATCH/STRING/NUMBER and AKSL_VERSION_SONAME.
AKSL_VERSION_NUMBER is computed rather than written as a literal, because a
literal 000100 is octal in C and would make 0.1.0 compare as 64;
test_version.c asserts it against the runtime components, so a rewrite to a
literal fails.

Those macros record what a caller was compiled against. aksl_version(),
aksl_version_string() and aksl_version_soname() report what actually loaded,
and AKSL_VERSION_CHECK() compares the two, raising AKERR_VALUE naming both.
It is a macro so that it expands at the caller's site and captures the
caller's numbers; the function compares them against the ones baked into the
library. Compatibility is "same soname", so patch is ignored -- a caller built
against 0.1.0 keeps working against 0.1.7.

Normally the soname catches a mismatch at load time and the check never fires.
It earns its keep when the soname is bypassed: a 0.2.0 build dropped in under
the 0.1 filename loads happily, and only the check notices.

write_basic_package_version_file() uses SameMinorVersion to mirror the soname,
falling back to ExactVersion below CMake 3.11 where that mode does not exist.
The fallback is stricter than the soname rule -- it pins the patch level too --
but never laxer, and wrongly refusing a good pairing beats wrongly accepting a
bad one.

Coverage of src/stdlib.c rose to 99.1% of lines (217/219), 45.1% of branches
and 25/25 functions. That puts branch coverage back over the old 45 gate, but
the gate stays at 40: 0.1 points of headroom is not a ratchet.

ctest 14/14, ASan+UBSan 14/14, coverage 16/16 at 90/40. Also verified out of
tree: SONAME libakstdlib.so.0.1 recorded in consumers, pkg-config
--modversion reporting 0.1.0, find_package(akstdlib 0.1) accepted with 0.2 and
1.0 refused, a patch-bumped 0.1.1 loading and passing the check, a 0.2.0
dropped in under the 0.1 filename caught by it, and an embedded
add_subdirectory build keeping its own version rather than the parent's.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 22:40:49 -04:00
07c448508b Upgrade to libakerror 1.0.0
Bump deps/libakerror 22 commits to 5ff8790 (1.0.0), which makes the
status-name table private, moves consumer status codes to a band starting
at AKERR_FIRST_CONSUMER_STATUS, enforces range ownership rather than
treating it as advisory, and gives the library an soname. See
deps/libakerror/UPGRADING.md.

src/stdlib.c needed no changes. This library defines no status codes of its
own -- it raises libakerror's AKERR_* codes and propagates errno, both
inside libakerror's reserved 0-255 band -- and it never referenced
AKERR_MAX_ERR_VALUE, __AKERR_ERROR_NAMES, AKERR_STATUS_RANGE_OK or
AKERR_STATUS_NAME_OK. What moved was everything around the code:

A -DAKSL_COVERAGE=ON build stopped configuring at all. libakerror
namespaces its `mutation` target when embedded but not its `coverage`
target, so it collided with ours. Shadow add_custom_target for the duration
of the add_subdirectory() call and rename the dependency's to
akerror_coverage, alongside the existing add_test shadow. Fix upstream and
delete the workaround; recorded in TODO.md.

Pin the 1.0.0 floor three ways, since no single one covers every
consumption path: an #error in akstdlib.h feature-testing
AKERR_FIRST_CONSUMER_STATUS, because libakerror publishes no version macro;
Requires: akerror >= 1.0.0 in akstdlib.pc, which also gets consumers
-lakerror transitively; and find_dependency(akerror) in
akstdlibConfig.cmake. The last was already broken before this bump -- the
template still carried its MyLibraryConfig placeholder with the dependency
commented out, so any external find_package(akstdlib) failed with a bare
"akerror::akerror not found" out of the generated targets file.

Branch coverage of src/stdlib.c fell from 51.0% to 44.3% with no source or
test change: the 1.0.0 PREPARE_ERROR/FAIL_* macros expand to more branches
at every call site, so 337/661 became 481/1087 -- 144 more branches covered,
426 more counted. Line coverage held at 99.0% (200/202) and function
coverage at 100% (21/21). Re-ratchet the CI branch gate 45 -> 40 rather than
chase branches that belong to libakerror's own suite.

tests/test_status_registry.c pins the contract that made the status-code
migration a no-op: libakstdlib reserves no consumer range, so an application
may allocate from AKERR_FIRST_CONSUMER_STATUS without coordinating with it,
and every status this library raises is inside the reserved band with a name
actually registered -- an unnamed one degrades to "Unknown Error" in every
later stack trace, which nothing else would notice. It exercises the new
ownership enforcement too, so the "reserves nothing" assertion cannot pass
vacuously.

ctest 13/13, ASan+UBSan 13/13, coverage 15/15 at 90/40, mutation 89.6%
(155/173, unchanged). Also verified out of tree: the #error fires as the
first diagnostic against a stale akerror.h, pkg-config refuses akerror
0.9.0, and an external find_package(akstdlib) consumer builds and runs
against a temp-prefix install.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 22:20:54 -04:00
a37ba3fb89 Namespace the coverage target when embedded
All checks were successful
libakstdlib CI Build / cmake_build (push) Successful in 2m47s
libakstdlib CI Build / coverage (push) Successful in 2m38s
libakstdlib CI Build / mutation_test (push) Successful in 9m17s
Follows a87cbfb: a sibling dependency may ship a `coverage` target of its
own, so a non-top-level build gets `akstdlib_coverage` instead. The
top-level name is unchanged, and so is every documented command.

Verified: cmake --build build-coverage --target coverage still runs the
suite (14/14) and reports 99.0% of lines; the mutation target still
resolves.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 02:08:25 -04:00
437da2960b Test the libc wrappers: 52% -> 99% line coverage
Every wrapper outside the list and tree code was untested. Six new test
files close that, following the plan already written in TODO.md 1.2-1.6:

  test_stream.c   fopen/fread/fwrite/fclose -- happy paths, the round
                  trip, AKERR_EOF on a short read, AKERR_IO on a stream
                  opened in the wrong mode, ENOENT, and the NULL guards
  test_format.c   printf/fprintf/sprintf -- text *and* count asserted
                  (stdout is pointed at a temp file to check aksl_printf),
                  all eight NULL guards, EBADF on a read-only stream, and
                  512 variadic calls in a loop as sanitizer cover for the
                  missing va_end
  test_convert.c  ato{i,l,ll,f} happy paths, negatives, leading
                  whitespace, NULL guards
  test_path.c     realpath on a file and on a symlink, both compared
                  against realpath(3) since TMPDIR may itself be a link;
                  ENOENT, ENOTDIR, NULL path
  test_strhash.c  djb2 known-answer vectors, len == 0, embedded NUL,
                  stability, NULL guards
  test_convert_strict.c
                  known-failing (2.1.5): the AKERR_VALUE / ERANGE
                  contract the ato* family cannot express today

test_tree.c gains the BFS AKERR_NOT_IMPLEMENTED contract, NULL arguments,
and a callback error that is not AKERR_ITERATOR_BREAK propagating out.

Tests deliberately say nothing about behaviour TODO.md records as
defective -- unchecked ptr/mode/resolved_path, short transfers reported as
success, *count left at -1, the djb2 sign extension -- so the eventual fix
does not have to come with a test rewrite. Each failure case in
test_path.c passes a zeroed buffer, because the wrapper's own error path
formats resolved_path with %s (2.1.6).

aksl_capture.h gains aksl_temp_file() with an atexit unlink backstop.
Without it every test that fails before its own unlink leaves temp files
behind -- which is the normal case for a known-failing test, and happens
173 times over in a mutation run.

Coverage on src/stdlib.c: 52.0% -> 99.0% of lines (200/202), 23.6% ->
51.0% of branches, 8/21 -> 21/21 functions. The two uncovered lines are
both `} HANDLE(e, AKERR_ITERATOR_BREAK) {`, where the macro starts with
the `break;` of PROCESS's `case 0:` arm -- reachable only via a non-NULL
error context whose status is zero, the pathology 2.2.1 exists to remove.

Mutation score on src/stdlib.c: 46.8% -> 89.6% (155/173 killed). CI, the
pre-push hook and the docs ratchet from 40 to 80 accordingly, and the 18
survivors are grouped by cause in TODO.md and README.md. A new CI
coverage job gates at 90% lines / 45% branches.

Verified:
  ctest --test-dir build            # 12/12
  ctest --test-dir build-asan       # 12/12 under ASan + UBSan
  ctest --test-dir build-coverage   # 14/14, report attached
  ctest --test-dir build -j8 --repeat until-fail:3
  gcc -Wall -Wextra -c on all nine test files  # no warnings
  python3 scripts/mutation_test.py --target src/stdlib.c  # 89.6%
No temp files left in /tmp after any of the above.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 02:07:36 -04:00
82c47ed773 Add code coverage to the CTest suite
New AKSL_COVERAGE option instruments the library and its tests with
--coverage -O0 and wires the report into the suite itself, so a plain
ctest --test-dir build-coverage both runs the tests and produces coverage.

Two CTest entries do the work, held in order by a CTest fixture rather
than by declaration order so they also hold under ctest -j:
coverage_reset (FIXTURES_SETUP) clears the .gcda counters before any test,
since gcov counts are cumulative and would otherwise fold in earlier runs;
coverage_report (FIXTURES_CLEANUP) aggregates gcov output afterwards.
AKSL_COVERAGE_THRESHOLD / AKSL_COVERAGE_BRANCH_THRESHOLD gate the report,
the same regression-ratchet idea as the mutation score. The `coverage`
target builds, runs and prints in one step.

scripts/coverage.py parses gcov's JSON output, aggregates line, branch and
function counts across translation units, and lists every uncovered line
and never-called function -- the actionable half, as with surviving
mutants. Python stdlib plus gcc's own gcov only: no lcov, gcovr or
genhtml. It also writes coverage-summary.txt (CTest hides the output of a
passing test) and a Cobertura coverage.xml for CI publishers.

Instrumentation is per target, so deps/libakerror stays out of the report.
The mutation harness now ignores build*/ and gcov artifacts when copying
the tree, so a coverage build does not slow it down.

Baseline on src/stdlib.c: 52.0% of lines, 23.6% of branches, 8 of 21
functions. The uncovered functions are the untested wrappers the mutation
survivors already point at (printf, ato*, stream, realpath, strhash).

Verified:
  cmake -S . -B build-coverage -DAKSL_COVERAGE=ON
  cmake --build build-coverage --target coverage      # 8/8, report printed
  ctest --test-dir build-coverage -j8                 # fixture order holds
  cmake -S . -B build-coverage -DAKSL_COVERAGE=ON -DAKSL_COVERAGE_THRESHOLD=60
  ctest --test-dir build-coverage --output-on-failure  # gate fails as expected
  ctest --test-dir build --output-on-failure           # 6/6, no .gcda emitted
  ctest --test-dir build-asan --output-on-failure      # 6/6
  scripts/mutation_test.py --target src/stdlib.c --list # 173 mutants, unchanged
Totals match gcov itself: 51.98% of 202 lines, 23.60% of 661 branches.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 02:07:35 -04:00
a87cbfb26d Namespace the embedded mutation target
All checks were successful
libakstdlib CI Build / cmake_build (push) Successful in 2m48s
libakstdlib CI Build / mutation_test (push) Successful in 8m0s
2026-07-29 18:02:21 -04:00
566004afd6 Add CLAUDE.md
All checks were successful
libakstdlib CI Build / cmake_build (push) Successful in 2m45s
libakstdlib CI Build / mutation_test (push) Successful in 8m2s
2026-07-29 17:42:38 -04:00
11c04923f8 Add AGENTS.md
All checks were successful
libakstdlib CI Build / cmake_build (push) Successful in 2m47s
libakstdlib CI Build / mutation_test (push) Successful in 8m3s
2026-07-29 17:29:49 -04:00
01734f511b Make error-status assertions authoritative
All checks were successful
libakstdlib CI Build / cmake_build (push) Successful in 2m46s
libakstdlib CI Build / mutation_test (push) Successful in 8m1s
Replace standalone message assertions with a helper that checks the returned akerr status first, then validates message content only as secondary context. Drop ambiguous memcpy message checks where both NULL guards use the same format string.
2026-07-29 15:47:17 -04:00
8003239116 Add memory wrapper tests
Cover deterministic TODO 1.1 memory-wrapper behavior with a new CTest target. Exercise malloc/free round trips, NULL argument handling, memset fill/no-op semantics, and memcpy copy/no-op/null-pointer paths.

Harden AKSL_CHECK_OK so success requires no returned error context, catching wrappers that accidentally raise status-0 errors from stale errno.

Co-authored-by: Codex GPT-5.5 Default <codex-gpt-5.5-default@openai.com>
2026-07-29 14:42:27 -04:00
50 changed files with 14819 additions and 1218 deletions

View File

@@ -17,18 +17,109 @@ jobs:
run: |
sudo apt-get update -y
sudo apt-get install -y cmake gcc moreutils
# Depends on libakerror@main
git clone https://source.starfort.tech/andrew/libakerror.git
cd libakerror
cmake -S . -B build
cmake --build build
cmake --install build
# This step used to clone and install libakerror@main. That was two
# different libakerrors depending on where you built: the install went to
# /usr/local, while the build below is top-level and so compiles
# deps/libakerror at the pinned commit via add_subdirectory -- the
# installed one was never actually linked against. TODO.md 2.3.
#
# The submodule wins. It is the version this repository pins, tests and
# ships against, and a CI that tests a different one is testing something
# nobody runs. It is installed here as well as compiled, because an
# installed libakstdlib is not usable without it: akstdlibConfig.cmake
# calls find_dependency(akerror), and the top-level build pulls the
# submodule in EXCLUDE_FROM_ALL so `cmake --install` on this project
# installs only this project.
- name: install the pinned libakerror
run: |
cmake -S deps/libakerror -B build-akerror
cmake --build build-akerror
sudo cmake --install build-akerror
- name: build and test
run: |
cmake -S . -B build
# -DAKSL_WERROR=ON here and not locally: a warning that stops the build
# mid-thought teaches people to turn warnings off, but a warning that
# reaches main is one nobody will ever look at again.
cmake -S . -B build -DAKSL_WERROR=ON
cmake --build build
sudo cmake --install build
ctest --test-dir build --output-on-failure
# Build something against what was just installed. The suite links the
# build tree, so it says nothing about whether the *installed* package is
# usable -- and the two have come apart before: find_dependency(akerror)
# resolving is a property of the install, not of the build.
- name: consume the installed package
run: |
sudo ldconfig
cmake -S tests/consumer -B build-consumer
cmake --build build-consumer
./build-consumer/consumer
- run: echo "🍏 This job's status is ${{ job.status }}."
# The sanitizer build is its own job rather than a step on the one above,
# because three of the defects fixed in 0.2.0 only ever misbehaved under
# instrumentation and the tests that pin them are worth failing on their own.
sanitizers:
runs-on: ubuntu-latest
steps:
- name: Check out repository code
uses: actions/checkout@v4
with:
submodules: recursive
- name: dependencies
run: |
sudo apt-get update -y
sudo apt-get install -y cmake gcc
- name: build and test under ASan + UBSan
run: |
cmake -S . -B build-asan -DAKSL_SANITIZE=ON -DAKSL_WERROR=ON
cmake --build build-asan
ctest --test-dir build-asan --output-on-failure
- run: echo "🍏 This job's status is ${{ job.status }}."
coverage:
runs-on: ubuntu-latest
steps:
- name: Check out repository code
uses: actions/checkout@v4
with:
submodules: recursive
- name: dependencies
run: |
sudo apt-get update -y
sudo apt-get install -y cmake gcc python3
# scripts/coverage.py needs nothing but python3 and gcc's own gcov, so
# there is no lcov/gcovr to install here.
#
# The gate is a ratchet, not a target. Across all four sources the suite
# covers 99.5% of lines (1708/1716), 46.0% of branches and 100% of
# functions (154/154), so 90/40 fails on a real regression -- a test
# deleted, or new untested code added -- without tripping over rounding.
# The report is printed either way; the uncovered lines it lists are the
# missing tests.
#
# Eight lines are uncovered and every one is deliberate: four
# `HANDLE(e, AKERR_ITERATOR_BREAK)` macro artifacts, the size_t overflow
# guard in strbuf_reserve (reaching it needs a buffer near SIZE_MAX), and
# the short transfer with neither feof nor ferror set, which the standard
# permits and Linux never produces. README.md has the detail.
#
# Branch coverage sits far below line coverage because most branches are
# inside libakerror's FAIL_*/ATTEMPT/FINISH expansions -- pool exhaustion,
# stack-trace limits, akerr_valid_error_address failures -- which this
# library has no way to reach. Chasing them here would be testing
# libakerror's macros, which is libakerror's mutation suite's job: macros
# expand at the call site, so coverage cannot see them properly from
# either side. The gate stays at 40 for that reason and not for want of
# tests.
- name: coverage
run: |
cmake -S . -B build-coverage -DAKSL_COVERAGE=ON \
-DAKSL_COVERAGE_THRESHOLD=90 \
-DAKSL_COVERAGE_BRANCH_THRESHOLD=40
cmake --build build-coverage
ctest --test-dir build-coverage --output-on-failure
cat build-coverage/coverage-summary.txt
- run: echo "🍏 This job's status is ${{ job.status }}."
mutation_test:
@@ -45,22 +136,37 @@ jobs:
sudo apt-get update -y
sudo apt-get install -y cmake gcc moreutils python3
# Verify the tests actually catch bugs: break the library many ways and
# confirm the suite fails. Gated on src/stdlib.c (fast, deterministic);
# run the full default target locally for the macro header as well.
# confirm the suite fails.
#
# The threshold is a ratchet, not a quality bar. The score on the section
# 1.0 suite is 46.8% (81/173 killed): the list and tree functions are
# covered, and everything else in src/stdlib.c -- the printf family, the
# ato* family, realpath, the memory and stream wrappers -- has no tests
# yet, so its mutants survive. 40 leaves headroom for the runner while
# still failing on a real regression (tests deleted, or new untested code
# added). Raise it as TODO.md sections 1.1-1.9 land.
# A sample rather than the whole set. The four sources generate 1701
# mutants and each one is a full rebuild and re-run of the suite, which is
# hours. --max-mutants samples by even index, not at random, so the same
# 260 run every time and the gate is reproducible -- and sampling all four
# files beats exhausting one of them, which is what this job used to do.
#
# The threshold is a ratchet, not a quality bar. The measured score is
# 72.3% (188/260). 65 leaves headroom for the runner and for the sample
# shifting as sources change, while still failing on a real regression --
# a test deleted, or new untested code added.
#
# It is well below the 89.6% this job reported at 0.1.0, and that is a
# change in denominator rather than in the tests: that figure covered one
# 561-line file, this one covers four totalling 1716 lines, most of the
# new surface being argument validation whose mutants are frequently
# equivalent. `errno = 0` deleted from a wrapper whose libc call always
# sets errno cannot be distinguished by any test that could be written.
# The survivors worth acting on are named in TODO.md; raise the gate as
# they become assertions.
- name: mutation testing
run: |
python3 scripts/mutation_test.py \
--target src/stdlib.c \
--target src/string.c \
--target src/stream.c \
--target src/collections.c \
--max-mutants 260 \
--junit mutation-junit.xml \
--threshold 40
--threshold 65
# Publish even when the threshold gate fails, so survivors are visible --
# each one is a missing test. Display-only (fail_on_failure: false); the
# --threshold above is the gate. annotate_only avoids the Checks API 404

View File

@@ -9,8 +9,9 @@
#
# Runs by default, a few seconds all in:
#
# * default build + ctest
# * default build + ctest (with -Werror, as CI has it)
# * ASan/UBSan build + ctest
# * doxygen -- fails on an undocumented public function
#
# Opt in to the slow harness (~25 minutes -- it rebuilds and re-runs the whole
# suite once per mutant):
@@ -29,7 +30,7 @@ set -u
# Keep in sync with the --threshold in .gitea/workflows/ci.yaml, so a push that
# would fail CI fails here first.
MUTATION_THRESHOLD="${AKSL_MUTATION_THRESHOLD:-40}"
MUTATION_THRESHOLD="${AKSL_MUTATION_THRESHOLD:-65}"
ZERO_SHA=0000000000000000000000000000000000000000
@@ -72,22 +73,44 @@ run() {
fi
}
# -DAKSL_WERROR=ON here and not in a plain local build: a warning that stops you
# mid-thought teaches people to turn warnings off, but a warning that reaches
# main is one nobody will look at again. This is the last point where it is
# still cheap to fix.
echo "pre-push: default build + ctest"
run cmake -S . -B "$builddir/default"
run cmake -S . -B "$builddir/default" -DAKSL_WERROR=ON
run cmake --build "$builddir/default"
run ctest --test-dir "$builddir/default" --output-on-failure
echo "pre-push: sanitizer build + ctest"
run cmake -S . -B "$builddir/asan" -DAKSL_SANITIZE=ON
run cmake -S . -B "$builddir/asan" -DAKSL_SANITIZE=ON -DAKSL_WERROR=ON
run cmake --build "$builddir/asan"
run ctest --test-dir "$builddir/asan" --output-on-failure
# Doxygen runs with WARN_IF_UNDOCUMENTED and WARN_NO_PARAMDOC on and EXTRACT_ALL
# off, so the target fails when a public function, parameter or return value has
# nobody describing it. Skipped where doxygen is not installed rather than
# failing: it is a documentation check, not a build dependency.
if command -v doxygen > /dev/null 2>&1; then
echo "pre-push: doxygen"
run cmake --build "$builddir/default" --target docs
else
echo "pre-push: doxygen not installed, skipping the documentation check"
fi
if [ "${AKSL_HOOK_MUTATION:-0}" = "1" ]; then
echo "pre-push: mutation testing, threshold ${MUTATION_THRESHOLD}% (this takes a while)"
# Deliberately not wrapped in run(): this one is slow enough that you want
# to watch it make progress.
# Sampled, like CI: the full set is 1701 mutants and each is a rebuild plus
# a full test run. --max-mutants samples by even index rather than at
# random, so this is the same 260 CI gates on.
if ! python3 scripts/mutation_test.py \
--target src/stdlib.c \
--target src/string.c \
--target src/stream.c \
--target src/collections.c \
--max-mutants "${AKSL_HOOK_MUTANTS:-260}" \
--threshold "$MUTATION_THRESHOLD"; then
echo >&2
echo "pre-push: mutation score below ${MUTATION_THRESHOLD}%. Push aborted." >&2

33
.gitignore vendored Normal file
View File

@@ -0,0 +1,33 @@
# Build trees. The names are conventional rather than enforced -- AGENTS.md uses
# build/, build-asan/ and build-coverage/ -- so anything build* is ignored.
build*/
# Profiling output. -pg is no longer in the default build (see CMakeLists.txt),
# but a deliberate profiling run still drops these wherever it was started.
gmon.out
*.gcda
*.gcno
*.gcov
# Test and report artifacts written into the source root by CI-style invocations.
ctest-junit.xml
mutation-junit.xml
coverage-summary.txt
coverage.xml
# Emacs. The backup files and the lock symlinks both; a dangling .#file symlink
# pointing at user@host:pid is not something anyone wants in a diff.
*~
\#*\#
.\#*
# Editor and OS noise
*.swp
.DS_Store
compile_commands.json
# Generated API documentation and its warning log. `cmake --build build --target
# docs` writes both; the log is the actionable part and is empty when clean.
doxygen/
doxygen-warnings.log
Doxyfile.generated

156
AGENTS.md Normal file
View File

@@ -0,0 +1,156 @@
# Repository Guidelines
## Project Structure & Module Organization
`libakstdlib` is a C shared library that wraps libc calls and data structures in
`libakerror` error contexts.
Public API declarations live in `include/akstdlib.h`, which is also where the
Doxygen documentation lives -- the header is the contract, the sources carry the
reasoning. The implementation is split by domain:
| File | Covers |
|---|---|
| `src/stdlib.c` | memory, formatted output, string-to-number, realpath, djb2, and the list/tree traversal entry points |
| `src/string.c` | the `string.h` surface |
| `src/stream.c` | `stdio.h` beyond open/read/write/close |
| `src/collections.c` | list and tree operations, hash map, string buffer, FNV-1a |
| `src/aksl_internal.h` | shared internals; not installed, not public |
Tests are one-file CTest executables under `tests/test_<name>.c`, with shared
helpers in `tests/aksl_capture.h`. `tests/negative/` holds sources that must
*fail* to compile, and `tests/consumer/` is a standalone project built against
the *installed* package rather than the build tree -- see the testing section
below. CMake package templates are
in `cmake/` and `akstdlib.pc.in`. The vendored dependency is `deps/libakerror`;
update it as a submodule rather than editing generated files under `build/`,
`build-asan/` or `build-coverage/`.
## Build, Test, and Development Commands
Initialize dependencies before a fresh build:
```sh
git submodule update --init --recursive
cmake -S . -B build
cmake --build build
```
Run the normal suite with `ctest --test-dir build --output-on-failure`. Use the
instrumented build for memory and undefined-behavior checks:
```sh
cmake -S . -B build-asan -DAKSL_SANITIZE=ON
cmake --build build-asan
ctest --test-dir build-asan --output-on-failure
```
For coverage, configure a third tree; the report is part of that suite (the
`coverage_reset` / `coverage_report` CTest entries) and also lands in
`build-coverage/coverage-summary.txt`:
```sh
cmake -S . -B build-coverage -DAKSL_COVERAGE=ON
cmake --build build-coverage --target coverage
```
Run `cmake --build build --target mutation` only when you need the slower
mutation harness, and `cmake --build build --target docs` to regenerate the API
documentation -- that one fails if any public function, parameter or return value
is undocumented, so it is a check as well as a generator.
`.githooks/pre-push` runs the default build, the sanitizer build and the
documentation check before a push; enable it with
`git config core.hooksPath .githooks`. `rebuild.sh` installs to
`/home/andrew/local` and removes the local build directory, so treat it as a
local convenience script.
## Coding Style & Naming Conventions
Use C with 4-space indentation; existing files sometimes use tabs for continued
statements, so match the surrounding block. Public symbols use the `aksl_`
prefix, structs use `aksl_<Name>`, and tests use `test_<feature>.c` plus static
`test_<case>` functions. Preserve the `akerr_ErrorContext AKERR_NOIGNORE *`
return convention and the `PREPARE_ERROR` / `FAIL_*` / `SUCCEED_RETURN` pattern.
Four conventions hold across the whole library, and a new wrapper that breaks one
of them is wrong even if it compiles and passes:
- **A NULL out-param is a caller error**, not "don't care".
- **Finding nothing is success** -- searching functions write NULL or zero and
return NULL.
- **Truncation is a failure.** Take the destination's size, raise
AKERR_OUTOFBOUNDS, and write nothing rather than a prefix.
- **Clear `errno` before the wrapped call** and read it back through
`AKSL_ERRNO_OR`, so no error can carry status 0 -- which every downstream
`DETECT` reads as success.
Every new public function needs a Doxygen block on its **declaration** with
`@brief`, a `@param` per parameter, a `@throws` per status it can raise, and
`@return`. The `docs` target fails otherwise.
The build is `-Wall -Wextra` and CI adds `-Werror`. `-Wpedantic` is deliberately
off: libakerror's `FAIL_*` macros trip "ISO C99 requires at least one argument
for the ..." on their own expansion, not on anything at the call site.
## Testing Guidelines
Add a new test by creating `tests/test_mything.c` and adding `mything` to the
right list in `CMakeLists.txt`. `AKSL_TESTS` must exit zero.
`AKSL_WILL_FAIL_TESTS` are deliberate abort/contract tests.
`AKSL_KNOWN_FAILING_TESTS` assert defects that have an open issue; when one
starts unexpectedly passing, move it into `AKSL_TESTS` with the fix and close the
issue. Both of the
latter are currently empty -- all six confirmed defects are fixed -- but the
mechanism stays for the next one.
`tests/negative/` holds sources that must **fail** to compile. Each is an
`EXCLUDE_FROM_ALL` target built with `-Werror` and registered as a `WILL_FAIL`
CTest entry, so the test passes only when the compile fails. They cover the two
guarantees the compiler enforces and nothing else does: `AKERR_NOIGNORE` and the
format attributes. Drop either in a refactor and every ordinary test still
passes.
`tests/consumer/` is configured standalone against `CMAKE_PREFIX_PATH`, not as
part of this build, because being part of this build is what would let it pass
without testing anything. It covers the paths only an install has: the version
file, `find_dependency(akerror)` resolving, and the exported
`akstdlib::akstdlib` target. CI runs it after `cmake --install`.
Coverage is 99.5% of lines and 100% of functions across all four sources; CI
gates at 90 (line) / 40 (branch), so new code needs tests in the same commit. Run
`cmake --build build-coverage --target coverage` and check the uncovered-line
listing before proposing a change. Tests for behaviour an open issue records as
defective belong in `AKSL_KNOWN_FAILING_TESTS` asserting the *correct* contract —
do not pin current-but-wrong behaviour in `AKSL_TESTS`, since that turns the
eventual fix into a test failure.
## Commit & Pull Request Guidelines
Recent commits use short imperative summaries, for example `Add memory wrapper
tests` and `Make error-status assertions authoritative`. Keep commits focused
and include tests with behavior changes. Pull requests should describe the
changed API or behavior, list the CTest/sanitizer/mutation commands run, and
link the issue it closes.
## Agent-Specific Instructions
**Outstanding work goes in the issue tracker, not in a file.** Open an issue at
<https://source.starfort.tech/andrew/libakstdlib/issues> — `tea issues create
--repo andrew/libakstdlib` — naming the file and line, the functional
consequence, and what closing it would touch. Label it by kind and blast radius
and leave `status::grooming` on it until its scope and approach are settled.
**Do not add outstanding items to `TODO.md`**: that file is the record of where
the library stands, what is deliberately not wrapped, and which uncovered lines
are uncoverable rather than untested. A description of work still to do goes
stale the moment somebody does it, which is why the two are separated.
**A defect in a dependency is filed against that dependency.** `libakerror` has
a tracker on the same forge, and three defects that cost this library a
workaround each sat in this repository's own notes for months without anybody
upstream being able to see them. Comment the workaround at its site with the
words "filed upstream" and delete it when the fix lands.
Do not modify generated build trees, profiling artifacts, or untracked scratch
files unless explicitly asked. Prefer small, test-backed changes and update
`README.md` when changing documented workflows.

1
CLAUDE.md Normal file
View File

@@ -0,0 +1 @@
See @AGENTS.md

View File

@@ -1,14 +1,84 @@
cmake_minimum_required(VERSION 3.10)
project(akstdlib LANGUAGES C)
# The single source of truth for the version. It flows from here into
# include/akstdlib_version.h (generated from the .in template next to it), the
# shared library's VERSION/SOVERSION, the Version: field in akstdlib.pc, and
# akstdlibConfigVersion.cmake. Nothing else should spell a version number.
#
# 0.2.0, and the minor bump is an ABI break on purpose. Fixing the confirmed
# defects in TODO.md 2.1 changed documented behaviour and, in five places,
# signatures:
#
# aksl_realpath takes the destination's length; aksl_realpath_alloc is new
# aksl_list_pop takes the head by reference
# aksl_tree_iterate lost its `queue` parameter
# aksl_fread/fwrite take a required transferred-count out-param
# aksl_sprintf is gone; aksl_snprintf replaces it
#
# plus the ato* family, which now reports a bad conversion rather than returning
# 0. Pre-1.0 the soname is MAJOR.MINOR, so 0.1 and 0.2 are different ABIs and a
# consumer built against 0.1 will not silently load this. See UPGRADING.md.
project(akstdlib VERSION 0.2.0 LANGUAGES C)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -g -ggdb -pg")
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -g -ggdb -pg")
set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -g -ggdb -pg")
# The granularity at which the ABI is allowed to break, and therefore the
# soname: libakstdlib.so.0.1. Pre-1.0 that is MAJOR.MINOR, because a 0.x library
# makes no compatibility promise across a minor bump. At 1.0 this becomes
# ${PROJECT_VERSION_MAJOR} alone -- change it here, and AKSL_VERSION_SONAME in
# the header template follows automatically because it is configured from this.
if(PROJECT_VERSION_MAJOR EQUAL 0)
set(AKSL_SOVERSION "${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}")
else()
set(AKSL_SOVERSION "${PROJECT_VERSION_MAJOR}")
endif()
set(AKSL_GENERATED_INCLUDE_DIR "${CMAKE_CURRENT_BINARY_DIR}/generated/include")
set(AKSL_VERSION_HEADER "${AKSL_GENERATED_INCLUDE_DIR}/akstdlib_version.h")
configure_file(
"${CMAKE_CURRENT_SOURCE_DIR}/include/akstdlib_version.h.in"
"${AKSL_VERSION_HEADER}"
@ONLY
)
# This used to be three lines setting CMAKE_CXX_FLAGS and the linker flags to
# "-g -ggdb -pg". In a LANGUAGES C project the CXX variable reaches no compiler
# at all, so the debug flags never applied -- but -pg did reach the linkers, and
# every test binary that ran dropped a gmon.out in the working directory. Nobody
# was reading those profiles. CMAKE_BUILD_TYPE controls debug info now, which is
# what it is for; add -pg deliberately when you actually want to profile.
#
# Debug is the default for a bare `cmake -S . -B build` because this is a
# library under active development and a stripped -O3 build is not what anyone
# configuring it without an opinion is asking for.
if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
set(CMAKE_BUILD_TYPE Debug CACHE STRING "Build type" FORCE)
message(STATUS "CMAKE_BUILD_TYPE was unset; defaulting to Debug")
endif()
# Warnings. The only warning in the tree when these went on was the unused
# `queue` parameter of aksl_tree_iterate, which TODO.md 2.2.8 had already
# recorded as a dead parameter -- so the cost of turning them on was one
# already-known defect, and the cost of leaving them off was every future one.
#
# -Wpedantic is deliberately not here. libakerror's FAIL_* macros take a message
# plus varargs, and a call with a bare message and no arguments trips "ISO C99
# requires at least one argument for the ...", which is a complaint about the
# macro's shape rather than about anything at this call site. Every FAIL_* in
# src/stdlib.c passes at least one argument regardless -- see the note in
# TODO.md 2.3 -- so the code is pedantic-clean; it is the expansion that is not.
if(CMAKE_C_COMPILER_ID MATCHES "^(GNU|Clang|AppleClang)$")
add_compile_options(-Wall -Wextra)
# CI turns this on. Locally it is off, because a warning that stops the build
# while you are in the middle of something is a good way to teach people to
# turn warnings off.
option(AKSL_WERROR "Treat compiler warnings as errors" OFF)
if(AKSL_WERROR)
add_compile_options(-Werror)
endif()
endif()
# Sanitizer build, off by default:
# cmake -S . -B build-asan -DAKSL_SANITIZE=ON && ctest --test-dir build-asan
# Set before the dependency is added so libakerror is instrumented too --
# several of the defects in TODO.md section 2 (the uninitialised %s in
# several of the defects listed in UPGRADING.md (the uninitialised %s in
# aksl_realpath, the unbounded vsprintf in aksl_sprintf, the missing va_end in
# the printf family) only show up under ASan/UBSan.
option(AKSL_SANITIZE "Build the library and its tests with ASan + UBSan" OFF)
@@ -20,6 +90,50 @@ if(AKSL_SANITIZE)
message(STATUS "AKSL_SANITIZE=ON: building with ASan + UBSan")
endif()
# Coverage build, off by default:
# cmake -S . -B build-coverage -DAKSL_COVERAGE=ON
# cmake --build build-coverage --target coverage
# Instrumentation is applied per target below (the library and the test
# binaries) rather than through CMAKE_C_FLAGS, so deps/libakerror is left
# uninstrumented -- it has its own suite, and its .gcda would only be noise in
# this project's report.
option(AKSL_COVERAGE "Build the library and its tests with gcov instrumentation" OFF)
# Minimum total line / branch coverage for the `coverage_report` CTest entry
# that a -DAKSL_COVERAGE=ON build adds. 0 disables the gate and reports only.
set(AKSL_COVERAGE_THRESHOLD 0 CACHE STRING
"Fail the coverage_report test below this total line coverage percentage")
set(AKSL_COVERAGE_BRANCH_THRESHOLD 0 CACHE STRING
"Fail the coverage_report test below this total branch coverage percentage")
if(AKSL_COVERAGE)
if(NOT CMAKE_C_COMPILER_ID MATCHES "^(GNU|Clang|AppleClang)$")
message(FATAL_ERROR
"AKSL_COVERAGE=ON needs a gcov-compatible compiler; "
"CMAKE_C_COMPILER_ID is ${CMAKE_C_COMPILER_ID}")
endif()
# -O0 because the optimizer folds and reorders lines until per-line counts
# stop matching the source. -fprofile-abs-path makes gcov record absolute
# source paths, which is what lets the report be read from any directory.
set(AKSL_COVERAGE_COMPILE_FLAGS --coverage -O0 -g)
if(CMAKE_C_COMPILER_ID STREQUAL "GNU")
list(APPEND AKSL_COVERAGE_COMPILE_FLAGS -fprofile-abs-path)
endif()
message(STATUS "AKSL_COVERAGE=ON: instrumenting akstdlib and its tests for gcov")
endif()
# Add gcov instrumentation to one target. No-op unless AKSL_COVERAGE is set.
function(aksl_target_coverage target)
if(NOT AKSL_COVERAGE)
return()
endif()
target_compile_options(${target} PRIVATE ${AKSL_COVERAGE_COMPILE_FLAGS})
if(CMAKE_VERSION VERSION_LESS 3.13)
set_property(TARGET ${target} APPEND_STRING PROPERTY LINK_FLAGS " --coverage")
else()
target_link_options(${target} PRIVATE --coverage)
endif()
endfunction()
if(TARGET akerror::akerror)
message(STATUS "FOUND akerror::akerror")
else()
@@ -52,6 +166,22 @@ if(CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR)
endif()
endfunction()
# libakerror namespaces its `mutation` target when it is embedded but not its
# `coverage` target (deps/libakerror/CMakeLists.txt:172 vs :189), so a
# -DAKSL_COVERAGE=ON build hits "another target with the same name already
# exists" against the `coverage` target this project adds, and fails to
# configure at all. Rename the dependency's on the way past rather than
# dropping it: its coverage script drives its own instrumented build tree, so
# `cmake --build build-coverage --target akerror_coverage` still does the right
# thing. Remove this once the dependency namespaces it upstream -- see TODO.md.
function(add_custom_target _name)
if(AKSL_SUPPRESS_ADD_TEST AND _name STREQUAL "coverage")
_add_custom_target(akerror_coverage ${ARGN})
else()
_add_custom_target(${ARGV})
endif()
endfunction()
add_subdirectory(deps/libakerror EXCLUDE_FROM_ALL)
set(AKSL_SUPPRESS_ADD_TEST FALSE)
@@ -69,20 +199,42 @@ set(libdir "\${exec_prefix}/lib")
set(includedir "\${prefix}/include")
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/akstdlib.pc.in ${CMAKE_CURRENT_BINARY_DIR}/akstdlib.pc @ONLY)
# One translation unit per surface. src/stdlib.c was the whole library when it
# covered 16 libc calls; it is split by domain now that it does not, so that a
# file can still be read start to finish. src/aksl_internal.h carries what they
# share and is not installed.
add_library(akstdlib SHARED
src/stdlib.c
src/string.c
src/stream.c
src/collections.c
)
add_library(akstdlib::akstdlib ALIAS akstdlib)
# Specify include directories for the library's headers (if applicable)
# Specify include directories for the library's headers (if applicable).
# The generated directory carries akstdlib_version.h, which akstdlib.h includes;
# it is PUBLIC because consumers building against the build tree need it too. On
# install both headers land side by side in ${CMAKE_INSTALL_INCLUDEDIR}, so the
# install interface needs no second entry.
target_include_directories(akstdlib PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
$<BUILD_INTERFACE:${AKSL_GENERATED_INCLUDE_DIR}>
$<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}/>
)
# VERSION gives the real file (libakstdlib.so.0.1.0); SOVERSION gives the symlink
# and the ELF soname recorded in every consumer (libakstdlib.so.0.1), so a
# consumer built against 0.1 will not silently load an ABI-incompatible 0.2.
set_target_properties(akstdlib PROPERTIES
VERSION ${PROJECT_VERSION}
SOVERSION ${AKSL_SOVERSION}
)
target_link_libraries(akstdlib PUBLIC akerror::akerror)
aksl_target_coverage(akstdlib)
set(main_lib_dest "lib/my_library-${MY_LIBRARY_VERSION}")
install(TARGETS akstdlib
EXPORT akstdlibTargets
@@ -93,6 +245,7 @@ install(TARGETS akstdlib
)
install(FILES "include/akstdlib.h" DESTINATION "include/")
install(FILES "${AKSL_VERSION_HEADER}" DESTINATION "include/")
install(FILES ${CMAKE_CURRENT_BINARY_DIR}/akstdlib.pc DESTINATION "lib/pkgconfig/")
@@ -108,8 +261,31 @@ configure_package_config_file(
INSTALL_DESTINATION ${akstdlib_install_cmakedir}
)
# Without this, find_package(akstdlib 0.1 REQUIRED) is refused for want of a
# version file no matter what is installed -- which is exactly the gap that
# stops cmake/akstdlib.cmake.in from requesting a version of akerror.
#
# SameMinorVersion mirrors the soname: pre-1.0, 0.1 and 0.2 are different ABIs.
# It arrived in CMake 3.11 and this project declares 3.10, so fall back to
# ExactVersion on older CMake. That is stricter than the soname rule -- it pins
# the patch level too, so a 0.1.0 request refuses a compatible 0.1.1 -- but it is
# never laxer, and wrongly refusing a good pairing beats wrongly accepting a bad
# one. Drop the branch when the minimum moves past 3.11.
if(CMAKE_VERSION VERSION_LESS 3.11)
set(AKSL_VERSION_COMPATIBILITY ExactVersion)
else()
set(AKSL_VERSION_COMPATIBILITY SameMinorVersion)
endif()
write_basic_package_version_file(
"${CMAKE_CURRENT_BINARY_DIR}/akstdlibConfigVersion.cmake"
VERSION ${PROJECT_VERSION}
COMPATIBILITY ${AKSL_VERSION_COMPATIBILITY}
)
install(FILES
"${CMAKE_CURRENT_BINARY_DIR}/akstdlibConfig.cmake"
"${CMAKE_CURRENT_BINARY_DIR}/akstdlibConfigVersion.cmake"
DESTINATION ${akstdlib_install_cmakedir}
)
@@ -122,33 +298,103 @@ install(FILES
# reaching FINISH_NORETURN, a deliberate contract
# violation), so a non-zero exit is a pass.
# AKSL_KNOWN_FAILING_TESTS assert the *correct* behaviour of a confirmed
# defect from TODO.md section 2.1. They fail until
# defect; see UPGRADING.md. They fail until
# the defect is fixed, and are marked WILL_FAIL so
# the suite stays green and the gap stays visible.
# When one is fixed CTest reports it as failed with
# "unexpectedly passed" -- that is the cue to move
# it up into AKSL_TESTS.
set(AKSL_TESTS
collections
convert
format
hashmap
linkedlist
memory
path
pool
status_registry
strbuf
stream
streamio
strhash
string
strto
tree
version
)
set(AKSL_WILL_FAIL_TESTS
)
# Empty, and that is the news. It held four entries -- convert_strict,
# list_append_chain, list_iterate_head and tree_iterate_break -- one for each
# confirmed defect in TODO.md 2.1. All four are fixed, so each of those files
# was folded back into the test for the thing it was testing (tests/
# test_convert.c, tests/test_linkedlist.c and tests/test_tree.c) where it now
# has to keep passing rather than merely keep failing visibly.
set(AKSL_KNOWN_FAILING_TESTS
list_append_chain # TODO.md 2.1.1 -- append truncates lists of 2+ nodes
list_iterate_head # TODO.md 2.1.2 -- iterate starts at the midpoint
tree_iterate_break # TODO.md 2.1.3 -- ITERATOR_BREAK does not stop a walk
)
foreach(_test IN LISTS AKSL_TESTS AKSL_WILL_FAIL_TESTS AKSL_KNOWN_FAILING_TESTS)
add_executable(test_${_test} tests/test_${_test}.c)
target_include_directories(test_${_test} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/tests)
target_link_libraries(test_${_test} PRIVATE akstdlib)
# The test binaries are instrumented too. The default report filters them out
# (only src/ and include/ are shown), but `scripts/coverage.py --include
# tests` then answers a different question: whether every test case and
# helper branch in tests/ actually runs, which is how a test function that was
# written but never wired into AKSL_RUN shows up.
aksl_target_coverage(test_${_test})
add_test(NAME ${_test} COMMAND test_${_test})
list(APPEND AKSL_TEST_TARGETS test_${_test})
endforeach()
# Negative compile tests -- TODO.md 1.3 and 1.9.
#
# Two properties of this library are enforced by the compiler and by nothing
# else: AKERR_NOIGNORE makes discarding a returned error context an error, and
# AKSL_PRINTF_FORMAT restores the format/argument checking a caller loses by
# going through a variadic wrapper. Both are attributes on declarations in
# include/akstdlib.h. If either is dropped in a refactor, every test still
# passes, the library still builds, and nothing looks wrong -- the guarantee just
# quietly stops existing.
#
# So each is asserted by a source file that must *fail* to compile. The target
# is EXCLUDE_FROM_ALL and built with -Werror; the CTest entry runs that build and
# is marked WILL_FAIL, so a successful compile fails the suite.
#
# Only where the compiler supports the attributes at all -- elsewhere
# AKSL_PRINTF_FORMAT expands to nothing and the file would compile correctly.
if(CMAKE_C_COMPILER_ID MATCHES "^(GNU|Clang|AppleClang)$")
foreach(_neg noignore format_mismatch)
add_executable(negative_${_neg} EXCLUDE_FROM_ALL tests/negative/${_neg}.c)
target_link_libraries(negative_${_neg} PRIVATE akstdlib)
target_compile_options(negative_${_neg} PRIVATE -Werror)
add_test(NAME negative_${_neg}
COMMAND ${CMAKE_COMMAND} --build ${CMAKE_CURRENT_BINARY_DIR}
--target negative_${_neg})
set_tests_properties(negative_${_neg} PROPERTIES
WILL_FAIL TRUE
TIMEOUT 120
# These invoke the build system, so they must not run while another build
# of the same tree is in flight.
RUN_SERIAL TRUE)
endforeach()
endif()
# tests/test_memory.c asks for SIZE_MAX/2 bytes to check that a refused
# allocation reports ENOMEM and leaves *dst NULL. Plain malloc returns NULL and
# sets ENOMEM, which is the case under test; ASan's allocator instead treats a
# request that large as a bug in the caller and aborts the process before malloc
# ever returns. allocator_may_return_null=1 restores the libc behaviour for this
# one binary, so the sanitizer build tests the same contract as the normal one
# rather than skipping it.
if(AKSL_SANITIZE AND "memory" IN_LIST AKSL_TESTS)
set_tests_properties(memory PROPERTIES
ENVIRONMENT "ASAN_OPTIONS=allocator_may_return_null=1")
endif()
if(AKSL_WILL_FAIL_TESTS OR AKSL_KNOWN_FAILING_TESTS)
set_tests_properties(
${AKSL_WILL_FAIL_TESTS} ${AKSL_KNOWN_FAILING_TESTS}
@@ -167,17 +413,133 @@ set_tests_properties(
PROPERTIES TIMEOUT 30
)
# Both the coverage report and the mutation harness are Python scripts.
find_package(Python3 COMPONENTS Interpreter)
# Code coverage. A -DAKSL_COVERAGE=ON build wires the report into the suite
# itself, so `ctest --test-dir build-coverage` both runs the tests and prints
# what they touched:
#
# coverage_reset deletes the accumulated .gcda counters. gcov counts are
# cumulative, so without this every report would fold in
# earlier runs and overstate coverage. FIXTURES_SETUP makes
# CTest run it before any test that needs the fixture, even
# under `ctest -j`.
# coverage_report aggregates gcov output and prints the summary plus the
# uncovered lines. FIXTURES_CLEANUP makes CTest run it after
# the last test in the fixture, which is exactly when the
# counters are complete.
#
# The report is a plain report unless AKSL_COVERAGE_THRESHOLD is set, in which
# case coverage_report fails below that percentage. Same ratchet idea as the
# mutation threshold: gate on the number you have, raise it as tests land.
#
# CTest hides the output of a passing test, so coverage_report also writes
# <build>/coverage-summary.txt and <build>/coverage.xml (Cobertura). The
# `coverage` target below builds, runs and prints in one step.
if(AKSL_COVERAGE AND Python3_FOUND)
set(AKSL_COVERAGE_ARGS
${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/scripts/coverage.py
--build ${CMAKE_CURRENT_BINARY_DIR}
--source-root ${CMAKE_CURRENT_SOURCE_DIR})
add_test(NAME coverage_reset COMMAND ${AKSL_COVERAGE_ARGS} --zero)
add_test(NAME coverage_report COMMAND ${AKSL_COVERAGE_ARGS}
--threshold ${AKSL_COVERAGE_THRESHOLD}
--branch-threshold ${AKSL_COVERAGE_BRANCH_THRESHOLD}
--output ${CMAKE_CURRENT_BINARY_DIR}/coverage-summary.txt
--cobertura ${CMAKE_CURRENT_BINARY_DIR}/coverage.xml)
set_tests_properties(coverage_reset PROPERTIES FIXTURES_SETUP AKSL_GCDA)
set_tests_properties(coverage_report PROPERTIES FIXTURES_CLEANUP AKSL_GCDA)
set_tests_properties(
${AKSL_TESTS} ${AKSL_WILL_FAIL_TESTS} ${AKSL_KNOWN_FAILING_TESTS}
PROPERTIES FIXTURES_REQUIRED AKSL_GCDA
)
# gcov has to be spawned once per .gcda, so give the report more room than the
# 30s the test binaries get.
set_tests_properties(coverage_reset coverage_report PROPERTIES TIMEOUT 300)
# Namespaced when embedded in another project, for the same reason the mutation
# target below is: a sibling dependency may well ship a `coverage` target too.
if(CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR)
set(AKSL_COVERAGE_TARGET coverage)
else()
set(AKSL_COVERAGE_TARGET akstdlib_coverage)
endif()
# Convenience entry point that also builds the test binaries first. It runs
# the suite rather than the script directly, because the two fixture tests
# above already reset the counters and produce the report -- and CTest pulls a
# required fixture back in even when it is filtered out, so there is no way to
# run the tests without them. The second command re-reads the same counters to
# print the report that CTest suppressed for the passing coverage_report test
# (it only spawns gcov again, it does not re-run anything).
add_custom_target(${AKSL_COVERAGE_TARGET}
COMMAND ctest --test-dir ${CMAKE_CURRENT_BINARY_DIR} --output-on-failure
COMMAND ${AKSL_COVERAGE_ARGS}
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
USES_TERMINAL
COMMENT "Running the test suite under gcov and reporting coverage"
)
add_dependencies(${AKSL_COVERAGE_TARGET} ${AKSL_TEST_TARGETS})
elseif(AKSL_COVERAGE)
message(WARNING "AKSL_COVERAGE=ON but Python3 was not found: "
"instrumenting the build, but the coverage report "
"(scripts/coverage.py) will not be wired into CTest")
endif()
# API documentation:
# cmake --build build --target docs
#
# The Doxyfile carries no PROJECT_NUMBER. The version lives in exactly one place
# -- the project() call at the top of this file -- and a version written into the
# Doxyfile as well would be a second place to forget. Doxygen reads its
# configuration from stdin when given "-", so the number is appended on the way
# past and the checked-in file stays version-free.
#
# WARN_IF_UNDOCUMENTED and WARN_NO_PARAMDOC are both on, and EXTRACT_ALL is off:
# an undocumented function is a warning rather than a silently empty page. The
# target fails if the log is non-empty, which is what makes "documented" a
# property the build checks rather than an impression.
find_program(AKSL_DOXYGEN doxygen)
if(AKSL_DOXYGEN)
if(CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR)
set(AKSL_DOCS_TARGET docs)
else()
set(AKSL_DOCS_TARGET akstdlib_docs)
endif()
add_custom_target(${AKSL_DOCS_TARGET}
COMMAND ${CMAKE_COMMAND} -E echo
"Generating API documentation for akstdlib ${PROJECT_VERSION}"
COMMAND ${CMAKE_COMMAND}
-DAKSL_DOXYGEN=${AKSL_DOXYGEN}
-DAKSL_SOURCE_DIR=${CMAKE_CURRENT_SOURCE_DIR}
-DAKSL_VERSION=${PROJECT_VERSION}
-P ${CMAKE_CURRENT_SOURCE_DIR}/cmake/RunDoxygen.cmake
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
USES_TERMINAL
COMMENT "Running doxygen"
)
endif()
# Mutation testing: break the library in small ways and confirm the test suite
# notices. This is a meta-check on the tests themselves, and it rebuilds and
# re-runs the whole suite once per mutant, so it is a manual target rather than
# a CTest test:
# cmake --build build --target mutation
# When embedded in another project, use a namespaced target to avoid collisions
# with mutation targets provided by sibling dependencies.
# This target covers both src/stdlib.c and include/akstdlib.h. CI runs the
# narrower, faster src/stdlib.c set with a --threshold gate; see
# .gitea/workflows/ci.yaml.
find_package(Python3 COMPONENTS Interpreter)
if(Python3_FOUND)
add_custom_target(mutation
if(CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR)
set(AKSL_MUTATION_TARGET mutation)
else()
set(AKSL_MUTATION_TARGET akstdlib_mutation)
endif()
add_custom_target(${AKSL_MUTATION_TARGET}
COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/scripts/mutation_test.py
--source-root ${CMAKE_CURRENT_SOURCE_DIR}

View File

@@ -42,9 +42,7 @@ DOXYFILE_ENCODING = UTF-8
# title of most generated pages and in a few other places.
# The default value is: My Project.
PROJECT_NAME = "My Project"
# The PROJECT_NUMBER tag can be used to enter a project or revision number. This
PROJECT_NAME = "libakstdlib"
# could be handy for archiving the generated documentation or if some version
# control system is used.
@@ -54,9 +52,7 @@ PROJECT_NUMBER =
# for a project that appears at the top of each page and should give viewer a
# quick idea about the purpose of the project. Keep the description short.
PROJECT_BRIEF =
# With the PROJECT_LOGO tag one can specify a logo or an icon that is included
PROJECT_BRIEF = "libc wrappers that report failures through libakerror error contexts"
# in the documentation. The maximum height of the logo should not exceed 55
# pixels and the maximum width should not exceed 200 pixels. Doxygen will copy
# the logo to the output directory.
@@ -68,9 +64,7 @@ PROJECT_LOGO =
# entered, it will be relative to the location where doxygen was started. If
# left blank the current directory will be used.
OUTPUT_DIRECTORY =
# If the CREATE_SUBDIRS tag is set to YES then doxygen will create up to 4096
OUTPUT_DIRECTORY = doxygen
# sub-directories (in 2 levels) under the output directory of each output format
# and will distribute the generated files over these directories. Enabling this
# option can be useful when feeding doxygen a huge amount of source files, where
@@ -157,9 +151,7 @@ ABBREVIATE_BRIEF = "The $name class" \
# description.
# The default value is: NO.
ALWAYS_DETAILED_SEC = NO
# If the INLINE_INHERITED_MEMB tag is set to YES, doxygen will show all
ALWAYS_DETAILED_SEC = YES
# inherited members of a class in the documentation of that class as if those
# members were ordinary class members. Constructors, destructors and assignment
# operators of the base classes will not be shown.
@@ -209,9 +201,7 @@ SHORT_NAMES = NO
# description.)
# The default value is: NO.
JAVADOC_AUTOBRIEF = NO
# If the JAVADOC_BANNER tag is set to YES then doxygen will interpret a line
JAVADOC_AUTOBRIEF = YES
# such as
# /***************
# as being the beginning of a Javadoc-style comment "banner". If set to NO, the
@@ -291,9 +281,7 @@ ALIASES =
# members will be omitted, etc.
# The default value is: NO.
OPTIMIZE_OUTPUT_FOR_C = NO
# Set the OPTIMIZE_OUTPUT_JAVA tag to YES if your project consists of Java or
OPTIMIZE_OUTPUT_FOR_C = YES
# Python sources only. Doxygen will then generate output that is more tailored
# for that language. For instance, namespaces will be presented as packages,
# qualified scopes will look different, etc.
@@ -519,8 +507,6 @@ TIMESTAMP = NO
# The default value is: NO.
EXTRACT_ALL = NO
# If the EXTRACT_PRIVATE tag is set to YES, all private members of a class will
# be included in the documentation.
# The default value is: NO.
@@ -543,8 +529,6 @@ EXTRACT_PACKAGE = NO
# The default value is: NO.
EXTRACT_STATIC = NO
# If the EXTRACT_LOCAL_CLASSES tag is set to YES, classes (and structs) defined
# locally in source files will be included in the documentation. If set to NO,
# only classes defined in header files are included. Does not have any effect
# for Java sources.
@@ -681,9 +665,7 @@ INLINE_INFO = YES
# name. If set to NO, the members will appear in declaration order.
# The default value is: YES.
SORT_MEMBER_DOCS = YES
# If the SORT_BRIEF_DOCS tag is set to YES then doxygen will sort the brief
SORT_MEMBER_DOCS = NO
# descriptions of file, namespace and class members alphabetically by member
# name. If set to NO, the members will appear in declaration order. Note that
# this will also influence the order of the classes in the class list.
@@ -853,8 +835,6 @@ WARNINGS = YES
# The default value is: YES.
WARN_IF_UNDOCUMENTED = YES
# If the WARN_IF_DOC_ERROR tag is set to YES, doxygen will generate warnings for
# potential errors in the documentation, such as documenting some parameters in
# a documented function twice, or documenting parameters that don't exist or
# using markup commands wrongly.
@@ -877,9 +857,7 @@ WARN_IF_INCOMPLETE_DOC = YES
# WARN_IF_INCOMPLETE_DOC
# The default value is: NO.
WARN_NO_PARAMDOC = NO
# If WARN_IF_UNDOC_ENUM_VAL option is set to YES, doxygen will warn about
WARN_NO_PARAMDOC = YES
# undocumented enumeration values. If set to NO, doxygen will accept
# undocumented enumeration values. If EXTRACT_ALL is set to YES then this flag
# will automatically be disabled.
@@ -931,9 +909,7 @@ WARN_LINE_FORMAT = "at line $line of file $file"
# specified the warning and error messages are written to standard output
# (stdout).
WARN_LOGFILE =
#---------------------------------------------------------------------------
WARN_LOGFILE = doxygen-warnings.log
# Configuration options related to the input files
#---------------------------------------------------------------------------
@@ -943,9 +919,7 @@ WARN_LOGFILE =
# spaces. See also FILE_PATTERNS and EXTENSION_MAPPING
# Note: If this tag is empty the current directory is searched.
INPUT =
# This tag can be used to specify the character encoding of the source files
INPUT = include src
# that doxygen parses. Internally doxygen uses the UTF-8 encoding. Doxygen uses
# libiconv (or the iconv built into libc) for the transcoding. See the libiconv
# documentation (see:
@@ -1038,18 +1012,14 @@ FILE_PATTERNS = *.c \
# be searched for input files as well.
# The default value is: NO.
RECURSIVE = YES
# The EXCLUDE tag can be used to specify files and/or directories that should be
RECURSIVE = NO
# excluded from the INPUT source files. This way you can easily exclude a
# subdirectory from a directory tree whose root is specified with the INPUT tag.
#
# Note that relative paths are relative to the directory from which doxygen is
# run.
EXCLUDE =
# The EXCLUDE_SYMLINKS tag can be used to select whether or not files or
EXCLUDE = src/aksl_internal.h
# directories that are symbolic links (a Unix file system feature) are excluded
# from the input.
# The default value is: NO.
@@ -1968,9 +1938,7 @@ EXTRA_SEARCH_MAPPINGS =
# If the GENERATE_LATEX tag is set to YES, doxygen will generate LaTeX output.
# The default value is: YES.
GENERATE_LATEX = YES
# The LATEX_OUTPUT tag is used to specify where the LaTeX docs will be put. If a
GENERATE_LATEX = NO
# relative path is entered the value of OUTPUT_DIRECTORY will be put in front of
# it.
# The default directory is: latex.

406
README.md
View File

@@ -5,12 +5,64 @@
`libakstdlib` wraps C standard library functions so that they report failures
through [libakerror](https://source.starfort.tech/andrew/libakerror)'s
`ATTEMPT { ... } HANDLE { ... }` error contexts instead of through return codes
and `errno`. It also provides a few data structures built on the same
convention (a doubly-linked list and a binary tree).
and `errno`. It also provides data structures built on the same convention.
Every entry point returns `akerr_ErrorContext *` and is marked `AKERR_NOIGNORE`.
See `TODO.md` for the current state of the library: what is covered by tests,
which corner cases are still open, and which libc functions are not yet wrapped.
See `TODO.md` for the current state of the library and `UPGRADING.md` if you are
coming from 0.1.0, which this release breaks.
## What it wraps
| Area | Source | Functions |
|---|---|---|
| Memory | `src/stdlib.c` | `malloc` `calloc` `realloc` `free` `freep` `memset` `memcpy` `memmove` `memcmp` `memchr` |
| Formatted output | `src/stdlib.c` | `printf` `fprintf` `snprintf` and their `v*` forms |
| String → number | `src/stdlib.c` | `strtol` `strtoll` `strtoul` `strtoull` `strtod` `strtof` `strtold`, and `atoi` `atol` `atoll` `atof` on top of them |
| Paths and hashing | `src/stdlib.c` | `realpath` `realpath_alloc` `strhash_djb2` `strhash_djb2_str` |
| Strings | `src/string.c` | `strlen` `strnlen` `strcpy` `strncpy` `strcat` `strncat` `strdup` `strndup` `strcmp` `strncmp` `strcasecmp` `strncasecmp` `strcoll` `strchr` `strrchr` `strstr` `strcasestr` `strpbrk` `strspn` `strcspn` `strtok_r` `strsep` `strerror` |
| Streams | `src/stream.c` | `fopen` `fread` `fwrite` `fclose` `fseek` `ftell` `rewind` `fseeko` `ftello` `fgetpos` `fsetpos` `fflush` `setvbuf` `fgetc` `fputc` `ungetc` `fgets` `fputs` `getline` `getdelim` `feof` `ferror` `clearerr` `fileno` `freopen` `fdopen` `tmpfile` `sscanf` `fscanf` `remove` `rename` `mkstemp` `mkdtemp` |
| Collections | `src/collections.c` | doubly-linked list (bare-node and tracked-container forms), binary search tree, breadth- and depth-first traversal, fixed-capacity hash map, growable string buffer, FNV-1a |
### Where it deviates from libc, and why
The whole point is to make silent failures loud, so several wrappers are
deliberately stricter than the function they are named for. These are the ones
that will surprise you:
| Wrapper | Deviation |
|---|---|
| `aksl_free(NULL)` | `AKERR_NULLPOINTER`. `free(NULL)` is legal and does nothing; in a codebase that routes every allocation through `aksl_malloc`, a pointer you believed was live turning out NULL means something upstream did not happen. |
| `aksl_malloc(0, &p)` | `AKERR_VALUE`. There is nothing useful to hand back, and `malloc(0)` returning NULL without setting `errno` is how an error with status `0` used to get raised. |
| `aksl_atoi` and friends | Report bad conversions. `atoi(3)` has no error channel at all: junk converts to `0` and overflow wraps. Base 10, whole string, `ERANGE` on overflow. |
| `aksl_strcpy` / `strncpy` / `strcat` / `strncat` | Take the destination's size, which the libc originals cannot be called safely without. Truncation is `AKERR_OUTOFBOUNDS` and writes nothing. `aksl_strncpy` always terminates and never NUL-pads. |
| `aksl_snprintf` | Truncation is `AKERR_OUTOFBOUNDS`, not a short success. There is no `aksl_sprintf`: an error-handling wrapper around an unbounded write is the sharp edge this library exists to remove. |
| `aksl_memcpy` | Overlapping ranges are `AKERR_VALUE` rather than undefined behaviour. Use `aksl_memmove`. |
| `aksl_fread` / `aksl_fwrite` | Require a transferred-count out-param, and report a short transfer with no stream error as `AKERR_IO` rather than as success. |
| `aksl_sscanf` / `aksl_fscanf` | Take the number of conversions you expect. Comparing `scanf(3)`'s return against that by hand at every call site is the check everyone eventually forgets. |
| `aksl_realpath` | Takes the destination's length and refuses anything below `PATH_MAX`, because `realpath(3)` cannot be bounded. |
| `aksl_list_pop` | Takes the head by reference, because popping the head has to move it. |
| Searching (`strchr`, `strstr`, `memchr`, `aksl_list_find`, `aksl_hashmap_get`, …) | Finding nothing is **success** with a NULL or zero result, not an error. Absent is an ordinary answer. |
| `aksl_strtok` | Does not exist. `strtok(3)` keeps its state in a hidden static; use `aksl_strtok_r` or `aksl_strsep`. |
### Thread safety
**This library is not thread-safe, and cannot be made so from here.**
libakerror hands out error contexts from `AKERR_ARRAY_ERROR`, a process-global
array with no locking, and every entry point in this library takes a slot from it
on any failure path. Two threads raising errors concurrently can be handed the
same slot. `errno` is thread-local so the wrapped calls themselves are fine; the
error reporting is not.
There is no TSan test here because there is nothing to verify — the answer is
known and it is "no". Fixing it means locking or thread-local storage in
libakerror's pool, which is that library's decision to make; issue #2
records it. Until then: confine libakstdlib calls to one thread, or serialise
them yourself.
The wrappers add no state of their own beyond that. `aksl_strtok_r` and
`aksl_strsep` keep their state in the caller's `saveptr`, and no function here
uses a static buffer.
## Building
@@ -25,9 +77,110 @@ A top-level build compiles the vendored `deps/libakerror`. When `libakstdlib` is
consumed as a subproject, it uses whatever `akerror::akerror` target or installed
package the parent provides instead.
### This library's own version
`libakstdlib` is at **0.2.0**. The version lives in exactly one place — the
`project()` call in `CMakeLists.txt` — and flows from there into everything
else, so a bump is a one-line edit:
| Artifact | Value at 0.2.0 | From |
| --- | --- | --- |
| `AKSL_VERSION_MAJOR` / `_MINOR` / `_PATCH` | `0` / `2` / `0` | `include/akstdlib_version.h.in` |
| `AKSL_VERSION_STRING` | `"0.2.0"` | same |
| `AKSL_VERSION_NUMBER` | `200` | same |
| `AKSL_VERSION_SONAME` | `"0.2"` | same |
| shared library | `libakstdlib.so.0.2.0`, soname `libakstdlib.so.0.2` | `VERSION` / `SOVERSION` |
| `pkg-config --modversion akstdlib` | `0.2.0` | `akstdlib.pc` |
| `find_package(akstdlib 0.2)` | accepted; `0.1` and `1.0` refused | `akstdlibConfigVersion.cmake` |
`akstdlib_version.h` is **generated** — that is why there is no such file in the
source tree, only the `.in` template beside `akstdlib.h`. Don't hand-edit the
copy in your build directory; change the template or `project()`.
It is `0.x` deliberately. The 0.1 → 0.2 bump was itself an ABI break — fixing the
confirmed defects changed five signatures and the `ato*` contract, all of it
listed in `UPGRADING.md` — and the API is not being promised until the wishlist
in the tracker has settled. While the major version is `0`, **the soname carries
`MAJOR.MINOR`**: 0.1 and 0.2
are different ABIs and the loader will not substitute one for the other. At 1.0
the soname becomes `MAJOR` alone — the `if(PROJECT_VERSION_MAJOR EQUAL 0)` in
`CMakeLists.txt` and the matching `#if` in `tests/test_version.c` are the two
places that encode this, and they are tested against each other.
`AKSL_VERSION_NUMBER` is computed rather than written as a literal, because a
literal `000100` is *octal* in C and would make 0.1.0 compare as 64.
#### Compiled-against vs. loaded
The macros above record what a caller was **compiled** against. What it actually
**loaded** is a different question, and the two can disagree:
```c
int major, minor, patch;
akerr_ErrorContext *e = aksl_version(&major, &minor, &patch); /* the loaded .so */
const char *v = aksl_version_string(); /* likewise */
e = AKSL_VERSION_CHECK(); /* compares the two; AKERR_VALUE on a mismatch */
```
`AKSL_VERSION_CHECK()` is a macro on purpose: it expands at *your* call site, so
it captures the `AKSL_VERSION_*` you were built with and passes them to a
function that compares against the values baked into the library. Calling
`aksl_version_check()` with hand-written numbers defeats the whole mechanism.
Compatibility is defined as "same soname", so pre-1.0 both major and minor must
match and the patch level is ignored — a caller built against 0.2.0 keeps working
against 0.2.7, which is exactly the promise the shared soname makes.
In normal use the soname catches the mismatch first, at load time, and the check
never fires. It earns its keep when the soname is bypassed: a hand-install that
drops a 0.3.0 build in under the 0.2 filename, or a package that strips
versioning. Then the loader is happy and only the check notices:
```
compiled against : 0.2.0 (soname 0.2)
loaded : 0.3.0 (0.3.0)
MISMATCH DETECTED: compiled against libakstdlib 0.2.0, loaded 0.3.0 (soname 0.3)
```
### The libakerror version floor
**libakerror 1.0.0 or newer is required.** That release made the status-name
table private, moved consumer status codes into a band starting at
`AKERR_FIRST_CONSUMER_STATUS` (256), made range ownership enforced rather than
advisory, and gave the library an soname — see
`deps/libakerror/UPGRADING.md`. It is a source *and* ABI break, so pairing this
header with an older `akerror.h` is not a compile problem you can work around;
the pairing is simply invalid.
Three things enforce the floor, because no single one covers every way the
library gets consumed:
| Mechanism | Where | Catches |
| --- | --- | --- |
| `#error` on a missing `AKERR_FIRST_CONSUMER_STATUS` | `include/akstdlib.h` | a stale `akerror.h` earlier on the include path, at the first diagnostic rather than as a pile of errors inside `src/stdlib.c` |
| `Requires: akerror >= 1.0.0` | `akstdlib.pc.in` | a pkg-config consumer, which also now gets `-lakerror` transitively |
| `find_dependency(akerror)` | `cmake/akstdlib.cmake.in` | a `find_package(akstdlib)` consumer, which previously failed with a bare *"akerror::akerror not found"* out of the generated targets file |
The header guard feature-tests rather than version-tests because libakerror
publishes no version macro; `AKERR_FIRST_CONSUMER_STATUS` is the symbol 1.0.0
introduced, so its absence is what "older than 1.0.0" actually looks like. The
CMake path requests no version for the same kind of reason: libakerror installs
no `akerrorConfigVersion.cmake`, so `find_dependency(akerror 1.0.0)` would be
refused for want of a version file no matter which akerror is installed.
**libakstdlib defines no status codes of its own.** It raises libakerror's
`AKERR_*` codes and propagates the host's `errno` values, all of which live in
libakerror's reserved `0``255` band, so it reserves no range and an application
is free to allocate from `AKERR_FIRST_CONSUMER_STATUS` without coordinating with
it. `tests/test_status_registry.c` pins that, along with the requirement that
every status this library raises actually has a name registered — an unnamed one
degrades to `"Unknown Error"` in every later stack trace, which nothing else
would notice.
## Testing
There are three harnesses. The first two take seconds; the third takes about
There are four harnesses. The first three take seconds; the fourth takes about
half an hour.
### 1. The test suite
@@ -41,8 +194,26 @@ ctest --test-dir build --output-on-failure
Tests live one per file in `tests/test_<name>.c` and share the helpers in
`tests/aksl_capture.h``AKSL_CHECK()` for plain assertions (unlike `assert()`
it survives `-DNDEBUG`), `AKSL_CHECK_STATUS(call, expected)` to run a wrapper and
assert on the status it returns, and an `AKSL_RUN()` driver that additionally
fails any test which leaks a slot from libakerror's error pool.
assert on the status it returns, `aksl_temp_file()` for tests that need a real
file to work on, and an `AKSL_RUN()` driver that additionally fails any test which
leaks a slot from libakerror's error pool.
One file per area of the API: `memory`, `format` (the `printf` family), `convert`
(the `ato*` family) and `strto` (the family underneath it), `stream`
(`fopen`/`fread`/`fwrite`/`fclose`) and `streamio` (everything else in
`src/stream.c`), `string`, `path` (`aksl_realpath`), `strhash`, `linkedlist`,
`tree`, `collections` (the list and tree additions), `hashmap`, `strbuf`,
`version`, `status_registry` (this library's side of the libakerror
status-registry contract — see "The libakerror version floor" above), and `pool`.
`pool` is the odd one out: it asserts two cross-cutting properties rather than
any function's behaviour. Every failure path is driven `AKERR_MAX_ARRAY_ERROR + 10`
times with the pool checked after each round, because a wrapper that raises an
error and forgets to release it does not fail visibly — it fails a hundred-odd
calls later in whatever unrelated code asks for a slot next. And every error is
checked to name the function and file it was actually raised from, which is what
catches a `FAIL` that migrates into a shared helper during a refactor: the status
stays right, the message stays right, and the origin quietly starts lying.
To add a test, drop `tests/test_mything.c` in place and add `mything` to
`AKSL_TESTS` in `CMakeLists.txt`.
@@ -54,18 +225,53 @@ of them invert the meaning of "Passed":
|---|---|
| `AKSL_TESTS` | Ordinary tests. Must exit 0. |
| `AKSL_WILL_FAIL_TESTS` | Expected to abort by design — an unhandled error reaching `FINISH_NORETURN`, or a deliberate contract violation. Marked `WILL_FAIL`, so a non-zero exit is a pass. |
| `AKSL_KNOWN_FAILING_TESTS` | Assert the *correct* behaviour of a confirmed defect (see `TODO.md` §2.1). Also marked `WILL_FAIL`. |
| `AKSL_KNOWN_FAILING_TESTS` | Assert the *correct* behaviour of a confirmed defect (see `UPGRADING.md`). Also marked `WILL_FAIL`. |
So `ctest` reporting all green does **not** mean the library is defect-free — it
means the known-good tests passed and the known-bad ones are still failing in the
documented way. When a defect is fixed, its test starts passing, CTest reports it
as failed with *unexpectedly passed*, and that is the cue to move it from
`AKSL_KNOWN_FAILING_TESTS` into `AKSL_TESTS`.
**Both of those lists are currently empty**, which is the news: all six confirmed
defects recorded in `UPGRADING.md` are fixed, and the four tests that used to sit in
`AKSL_KNOWN_FAILING_TESTS` are folded back into the tests for the things they
test, where they now have to keep passing rather than keep failing visibly. The
mechanism stays for the next one. When a defect is fixed its known-failing test
starts passing, CTest reports it as failed with *unexpectedly passed*, and that
is the cue to move it into `AKSL_TESTS`.
Two more entries, `negative_noignore` and `negative_format_mismatch`, are
compile-time assertions rather than programs. Each builds a source file under
`tests/negative/` with `-Werror` and is marked `WILL_FAIL`, so the test passes
only when the compile *fails*. They exist because `AKERR_NOIGNORE` and
`AKSL_PRINTF_FORMAT` are enforced by the compiler and by nothing else: drop
either attribute in a refactor and every test still passes, the library still
builds, and the guarantee just quietly stops existing.
Every test is capped with a 30-second CTest `TIMEOUT`. The list and tree code is
full of loops whose termination hangs on a single condition, so a bug of that
shape hangs the suite rather than failing it.
### 1a. The installed package
```sh
cmake -S deps/libakerror -B build-akerror && cmake --build build-akerror
cmake --install build-akerror --prefix /some/prefix
cmake --install build --prefix /some/prefix
cmake -S tests/consumer -B build-consumer -DCMAKE_PREFIX_PATH=/some/prefix
cmake --build build-consumer && ./build-consumer/consumer
```
The suite links the build tree, so it says nothing about whether an *installed*
libakstdlib is usable. `tests/consumer/` is a standalone project that does the
things only an install exercises: `find_package(akstdlib 0.2)` against the
generated version file, `akstdlibConfig.cmake`'s `find_dependency(akerror)`, and
the exported `akstdlib::akstdlib` target. It touches one function from each of
the four sources, so a library installed with a source file missing from its link
line fails here rather than in whatever consumer finds it next.
Note that libakerror has to be installed too. A top-level build compiles the
vendored copy with `EXCLUDE_FROM_ALL`, so `cmake --install` on this project
installs only this project -- and an installed libakstdlib whose
`find_dependency(akerror)` cannot resolve is not usable. Install the submodule's
copy, not `libakerror@main`: that is the version this repository pins and tests
against, and it is what CI does.
### 2. Sanitizers
```sh
@@ -75,12 +281,125 @@ ctest --test-dir build-asan --output-on-failure
```
Builds the library, the tests and the vendored libakerror with ASan + UBSan and
`-fno-sanitize-recover=all`. Several of the open items in `TODO.md` §2 only
misbehave under instrumentation — the uninitialised `%s` in `aksl_realpath`, the
unbounded `vsprintf` behind `aksl_sprintf`, the missing `va_end` in the `printf`
family — so new tests for those should be run this way.
`-fno-sanitize-recover=all`. Three of the defects fixed in 0.2.0 only misbehaved
under instrumentation — the uninitialised `%s` in `aksl_realpath`'s error path,
the unbounded `vsprintf` behind the old `aksl_sprintf`, and the missing `va_end`
in the `printf` family — and the tests that pin them are written to be run this
way. `tests/test_path.c` deliberately passes an *uninitialised* buffer on every
failure path for exactly that reason.
### 3. Mutation testing
One test needs help from the sanitizer to test the same thing the normal build
does: `tests/test_memory.c` asks for `SIZE_MAX / 2` bytes to check that a refused
allocation reports `ENOMEM` and leaves `*dst` NULL. Plain `malloc` returns NULL;
ASan treats a request that large as a bug in the caller and aborts before `malloc`
returns at all. `CMakeLists.txt` sets `ASAN_OPTIONS=allocator_may_return_null=1`
for that one binary so the contract under test stays the same in both builds.
### 3. Code coverage
```sh
cmake -S . -B build-coverage -DAKSL_COVERAGE=ON
cmake --build build-coverage --target coverage
```
`-DAKSL_COVERAGE=ON` compiles the library and the tests with `--coverage -O0`,
and wires the report into the suite itself, so a plain
`ctest --test-dir build-coverage` also produces it. Two extra CTest entries
appear, held in place by a CTest fixture rather than by declaration order, so
they work under `ctest -j` too:
| Test | When | Does |
|---|---|---|
| `coverage_reset` | before every other test | deletes the accumulated `.gcda` counters |
| `coverage_report` | after every other test | aggregates `gcov` output, prints the summary, applies the threshold gate |
The reset matters: gcov counters are cumulative, so without it each report would
fold in every earlier run and overstate coverage.
`coverage` here is this project's target. libakerror ships a `coverage` target of
its own and, unlike its `mutation` target, does not namespace it when embedded,
so a top-level `-DAKSL_COVERAGE=ON` build would collide on the name and fail to
configure at all. `CMakeLists.txt` renames the dependency's to `akerror_coverage`
on the way past — it drives its own instrumented build tree, so
`cmake --build build-coverage --target akerror_coverage` still works. The
workaround goes away when libakerror namespaces it upstream; see issue #4.
CTest hides the output of a passing test, so `coverage_report` also writes
`build-coverage/coverage-summary.txt` (the same text report) and
`build-coverage/coverage.xml` (Cobertura, for CI publishers). The `coverage`
target above prints the report to the terminal for you; otherwise read the file
or use `ctest --test-dir build-coverage -V -R coverage_report`.
The report lists per-file line, branch and function coverage, then every
uncovered line and every function the suite never called — that listing is the
actionable part, the same way surviving mutants are for the harness below.
Drive the script directly for anything narrower:
```sh
scripts/coverage.py --build build-coverage # report on disk counters
scripts/coverage.py --build build-coverage --summary-only # totals only
scripts/coverage.py --build build-coverage --include tests # coverage of the tests themselves
scripts/coverage.py --build build-coverage --run-tests # reset, run ctest, report
scripts/coverage.py --build build-coverage --threshold 90 --branch-threshold 40
```
It needs nothing but Python 3 and gcc's own `gcov` — no lcov, gcovr or genhtml.
To gate on coverage, set the threshold at configure time; `coverage_report` then
fails below it, and the same regression-ratchet logic applies as for the mutation
score:
```sh
cmake -S . -B build-coverage -DAKSL_COVERAGE=ON \
-DAKSL_COVERAGE_THRESHOLD=90 -DAKSL_COVERAGE_BRANCH_THRESHOLD=40
```
**Where it stands.** All four sources, with the whole suite:
| file | lines | branches | functions |
|---|---|---|---|
| `src/collections.c` | 99.3% (601/605) | 43.5% | 100% (43/43) |
| `src/stdlib.c` | 99.4% (614/618) | 46.5% | 100% (55/55) |
| `src/stream.c` | 100% (282/282) | 43.4% | 100% (33/33) |
| `src/string.c` | 100% (211/211) | 53.8% | 100% (23/23) |
| **total** | **99.5% (1708/1716)** | **46.0%** | **100% (154/154)** |
so the 90/40 gate above is a ratchet with headroom rather than a target. Eight
lines are uncovered and each is uncovered on purpose:
- **Four `} HANDLE(e, AKERR_ITERATOR_BREAK) {`** lines. In libakerror that macro
begins with the `break;` belonging to `PROCESS`'s `case 0:` arm, which is only
reachable when a callback returns a non-NULL context whose status is *zero*
the pathological case §2.2.1 exists to remove. Left uncovered rather than
pinned by a test that would have to manufacture it.
- **Two in `strbuf_reserve`**, the `size_t` overflow guard on a doubling that
would wrap. Reaching it needs a buffer within a factor of two of `SIZE_MAX`,
which is not a test, it is a hang.
- **Two in `aksl_fread`/`aksl_fwrite`**, the short transfer with *neither* `feof`
nor `ferror` set. Every way of producing a short transfer on Linux sets one or
the other; the branch is there because the standard permits neither, not
because anything reaches it. Issue #6 records it as still open.
Branch coverage sits far below line coverage because most branches in these files
are inside the `FAIL_*`/`ATTEMPT`/`FINISH` macro expansions — pool exhaustion,
stack-trace buffer limits, `akerr_valid_error_address` failures — and belong to
libakerror's own suite rather than to this one. Every `FAIL_ZERO_RETURN` in the
tree contributes several branches that this library has no way to reach. The
libakerror 1.0.0 bump made that gap wider without changing a line here: the
branch denominator per call site grew, so identical tests scored lower. Chasing
the number would mean testing libakerror's macros, which is what libakerror's
mutation suite is for — macros expand at the call site, so coverage cannot see
them properly from either side.
Two caveats. Coverage is measured at `-O0`, because the optimizer reorders lines
until per-line counts stop matching the source — so a coverage build is not the
build to profile. And gcov flushes its counters at normal process exit, which an
`AKSL_WILL_FAIL_TESTS` entry that aborts by design never reaches: such a test
contributes no coverage data at all, so lines only it reaches are reported as
uncovered.
### 4. Mutation testing
The suite tells you the library works. Mutation testing tells you the *suite*
works: it breaks the library in small ways, one at a time, and checks that the
@@ -96,7 +415,7 @@ or drive the script directly for a faster or narrower run:
scripts/mutation_test.py --target src/stdlib.c # C source only
scripts/mutation_test.py --target src/stdlib.c --list # enumerate, build nothing
scripts/mutation_test.py --target src/stdlib.c --max-mutants 20
scripts/mutation_test.py --target src/stdlib.c --threshold 40
scripts/mutation_test.py --target src/stdlib.c --threshold 80
```
A mutant that makes the tests fail is *killed* (good); one the tests still pass
@@ -105,10 +424,49 @@ run prints every survivor with `file:line` and the exact edit. The harness never
touches your working tree — it copies the repo to a scratch directory and mutates
the copy.
CI runs the `src/stdlib.c` set with `--threshold 40`. That is a regression
ratchet rather than a quality bar: the current score is 46.8%, and the survivors
are concentrated in the wrappers that have no tests yet. Raise the threshold as
coverage lands.
CI runs a 260-mutant sample across all four sources with `--threshold 65`.
A sample, because 1701 mutants each needing a full rebuild and test run is
hours — and `--max-mutants` samples by *even index*, not at random, so the same
260 run every time and the gate is reproducible. Sampling all four files beats
exhausting one of them, which is what this job used to do.
**Where it stands: 72.3% (188/260 killed).** That is a ratchet with headroom,
not a target.
It is well below the 89.6% this project reported at 0.1.0, and the difference is
denominator rather than tests. That figure covered one 561-line file; this covers
four totalling 1716 lines, and most of the new surface is argument validation
whose mutants are frequently *equivalent* — a mutation that cannot change
observable behaviour, so no test could ever kill it. The clearest example:
```c
errno = 0; /* delete this line */
*dst = malloc(size);
FAIL_ZERO_RETURN(e, *dst, AKSL_ERRNO_OR(ENOMEM), "%zu bytes", size);
```
Deleting the `errno = 0` is undetectable, because `malloc` always sets `errno`
when it fails. The line is still right to have — it is what makes
`AKSL_ERRNO_OR`'s contract sound, and it matters for the calls that *don't* set
`errno` — but no test distinguishes the two versions. Twelve of the 72 survivors
are that line in twelve different wrappers.
The survivors do break down usefully:
| Survivors | What | Verdict |
|---|---|---|
| 12 | `errno = 0` deleted before a call that always sets `errno` | Equivalent. Not a missing test. |
| 14 | `FAIL_*` guards deleted or their constants shifted | Mixed — the constant shifts are undetectable where the test names the same constant symbolically; the deletions are real. |
| 7 | `SUCCEED_RETURN` deleted | The function falls off the end and returns whatever is in the return register, which is often NULL by luck. Needs an assertion on a side effect, not on the status. |
| 3 | `va_end` deleted | Undetectable on x86-64 SysV, where `va_end` is a no-op. Real UB, invisible here. |
| 3 | `FINISH(e, true)``FINISH(e, false)` | Real: an error swallowed instead of propagated. Worth a test. |
| 33 | the rest | Individually listed with `file:line` and the exact edit in the published report. |
Two of them were real gaps and are now fixed: the depth-first walk's `depth + 1`
on the *right* child (nothing had ever recursed right more than three deep, so a
right-leaning tree would have blown the stack the depth cap exists to protect),
and `aksl_tree_remove` on an empty tree, which without its guard dereferences
NULL. Both are in the suite now — which is what the harness is for.
## The pre-push hook
@@ -128,5 +486,5 @@ AKSL_HOOK_MUTATION=1 git push # also run the mutation gate (slow)
git push --no-verify # skip the hook entirely
```
Other knobs: `AKSL_MUTATION_THRESHOLD` (default 40, keep it in step with
Other knobs: `AKSL_MUTATION_THRESHOLD` (default 80, keep it in step with
`.gitea/workflows/ci.yaml`) and `AKSL_HOOK_BUILD_DIR`.

704
TODO.md
View File

@@ -1,553 +1,151 @@
# TODO
Working notes for `libakstdlib` — the akerror-wrapped libc surface.
Scope of the current library (`include/akstdlib.h`, `src/stdlib.c`): 16 libc wrappers
(`fopen`, `fread`, `fwrite`, `fclose`, `malloc`, `free`, `memset`, `memcpy`, `printf`,
`fprintf`, `sprintf`, `atoi`, `atol`, `atoll`, `atof`, `realpath`), one hash helper
(`aksl_strhash_djb2`), a doubly-linked list (`append`/`pop`/`iterate`) and a binary tree
iterator (`aksl_tree_iterate`).
Items marked **[CONFIRMED]** were reproduced by building the library and running a probe
program against it, not just read off the source.
---
## 1. Unit tests that should be written
### 1.0 Test-harness prerequisites — **DONE** (branch `feature/test-harness`)
`ctest` is green: 5/5, no *Not Run* entries. Everything in §1.11.9 can now be written
against `tests/aksl_capture.h` and added to the `AKSL_TESTS` list.
- [x] **The current tests cannot fail.** `tests/test_linkedlist.c` asserted nothing at all;
it printed node names and returned `0` unconditionally, so any of the list bugs in
§2.1 would pass it. Rewritten as 15 assertion-based cases covering the append, iterate
and pop behaviour that is correct today.
- [x] **`tests/test_tree.c` was red.** `ctest` reported `tree (Failed)`, exit 136, because
`parms.steps` was never reset between the three searches, so the second assertion
compared an accumulated `14` against `7`. Each search now builds its own tree and
params. The file records that its step counts do not actually distinguish the three
traversal orders — real order assertions remain §1.8.
- [x] **Adopt libakerror's test conventions.** `tests/aksl_capture.h` provides `AKSL_CHECK()`
(an `NDEBUG`-proof assert), `AKSL_CHECK_STATUS()`/`AKSL_CHECK_OK()` which run an
akerror-returning call, assert on its status and release the context,
`AKSL_CHECK_MSG_CONTAINS()`, a capturing `akerr_log_method`, `aksl_slots_in_use()` for
pool-leak checks, and an `AKSL_RUN()` driver that additionally fails any test which
leaks an error-pool slot.
- [x] **One test source per behaviour, registered with CTest via a list variable.**
`tests/test_<name>.c` → executable `test_<name>` → CTest test `<name>`, driven by
`AKSL_TESTS` in `CMakeLists.txt`.
- [x] **Add a `WILL_FAIL` category.** Two lists: `AKSL_WILL_FAIL_TESTS` for tests that abort
by design (empty for now) and `AKSL_KNOWN_FAILING_TESTS` for the confirmed defects in
§2.1 — see the note at the head of §2.1.
- [x] **Fix the four permanently-failing submodule tests.** `deps/libakerror` is added
`EXCLUDE_FROM_ALL`, so akerror's `add_test` entries registered but its test binaries
were never built, and `ctest` reported `err_catch`, `err_cleanup`, `err_trace`,
`err_improper_closure` as *Not Run* → failed on every run. Neither the pinned commit
nor upstream `main` guards that registration, CMake has no way to un-register a test,
and `set_tests_properties` cannot reach across directory scopes — so `add_test` and
`set_tests_properties` are shadowed for the duration of the `add_subdirectory` call.
The dependency has its own CI; this suite now contains only this project's tests.
- [x] **Wire in a memory-error build.** `cmake -S . -B build-asan -DAKSL_SANITIZE=ON` adds
`-fsanitize=address,undefined -fno-sanitize-recover=all` to the library, the tests and
the vendored libakerror. Currently clean, and it is the intended way to test the items
in §2 that only misbehave under instrumentation (uninitialised `%s` in
`aksl_realpath`, unbounded `vsprintf`, missing `va_end`).
- [x] **Port `scripts/mutation_test.py` from libakerror.** Retargeted at `src/stdlib.c` and
`include/akstdlib.h` (178 mutants) and exposed as
`cmake --build build --target mutation`. CI runs the narrower `src/stdlib.c` set (173
mutants) as its own `mutation_test` job with `--threshold 40` and a JUnit report.
**Current score: 46.8%** — 81 killed, 92 survived. The survivors are concentrated
exactly where §1.11.9 have yet to be written:
| surviving mutants | function |
|---|---|
| 10 | `aksl_tree_iterate` |
| 6 each | `aksl_fread`, `aksl_fwrite`, `aksl_fprintf`, `aksl_sprintf` |
| 5 | `aksl_printf` |
| 4 each | `aksl_memcpy`, `aksl_realpath`, `aksl_strhash_djb2`, `aksl_list_append` |
| 3 each | `aksl_malloc`, `aksl_free`, `aksl_memset`, `aksl_fopen`, `aksl_fclose`, `aksl_atoi`, `aksl_atol`, `aksl_atoll`, `aksl_atof` |
| 1 | `aksl_list_iterate` |
The threshold is a regression ratchet, not a quality bar; raise it as sections below
land. Each surviving mutant is a concrete missing test — `mutation-junit.xml` lists
them with `file:line` and the exact edit.
- [x] **Cap every test with a CTest `TIMEOUT`.** Found while measuring the above: a mutant
that deletes `fast = fast->next->next` turns `aksl_list_iterate` into an infinite
loop, so `ctest` waited forever, the mutation harness killed `ctest` at its own
120 s timeout, and the orphaned test binary kept spinning at 100% CPU while holding
the pipe open — the run wedged and had to be killed by hand. `TIMEOUT 30` on every
test makes `ctest` reap its own child, so those mutants are now killed in 30 s and
leave nothing behind. It also guards the real suite against the same class of bug.
- [x] **CI could not have run any of this.** `actions/checkout@v4` was not fetching
submodules, so `add_subdirectory(deps/libakerror)` had nothing to descend into and the
configure step failed before ever reaching the tests. Added `submodules: recursive`,
and swapped `cmake --build build --target test` for `ctest --output-on-failure` so a
failure is diagnosable from the job log. The deeper problem — CI installing
`libakerror@main` while the build actually compiles the pinned submodule — is §2.3.
### 1.1 Memory wrappers
- [ ] `aksl_malloc` — happy path returns non-NULL and writes through `dst`.
- [ ] `aksl_malloc(n, NULL)``AKERR_NULLPOINTER`, message content asserted.
- [ ] `aksl_malloc(0, &p)` — pin down the contract. Today the result depends on whether the
platform's `malloc(0)` returns NULL; if it does, the wrapper reports `errno`, which may
be `0`. See §2.2.1.
- [ ] `aksl_malloc(SIZE_MAX, &p)` → allocation failure surfaces `ENOMEM`, and `*dst` is left
NULL rather than garbage.
- [ ] `aksl_free(NULL)``AKERR_NULLPOINTER` (assert the documented behaviour, since
`free(NULL)` is legal C and callers will be surprised).
- [ ] `aksl_free` happy path, and a malloc→free round trip under ASan.
- [ ] `aksl_memset` — happy path fills the buffer; `n == 0` is a no-op; `s == NULL`
`AKERR_NULLPOINTER`.
- [ ] `aksl_memcpy` — happy path copies; `d == NULL` and `s == NULL` each →
`AKERR_NULLPOINTER`; `n == 0` with valid pointers succeeds.
- [ ] `aksl_memcpy` with overlapping regions — decide and test whether this is rejected
(`AKERR_VALUE`) or documented as UB like `memcpy`.
### 1.2 File I/O
- [ ] `aksl_fopen` happy path on a temp file; `*fp` is written.
- [ ] `aksl_fopen("/nonexistent/path", "r", &fp)``ENOENT` propagated as the status, with
the pathname in the message.
- [ ] `aksl_fopen` on a mode-denied path (e.g. `/proc/1/mem`, or a `chmod 000` temp file) →
`EACCES`.
- [ ] `aksl_fopen(path, mode, NULL)``AKERR_NULLPOINTER`.
- [ ] `aksl_fopen(NULL, "r", &fp)` and `aksl_fopen(path, NULL, &fp)` → currently unchecked,
see §2.2.2. Test once the guards exist.
- [ ] `aksl_fread` full read; short read at EOF → `AKERR_EOF`; read from a write-only stream
`AKERR_IO`; `fp == NULL``AKERR_NULLPOINTER`; `ptr == NULL` → should be
`AKERR_NULLPOINTER` (see §2.2.3).
- [ ] `aksl_fread` **partial read that is neither EOF nor error** — assert whatever the fixed
contract is; today this silently returns success and the caller cannot tell how many
members were read.
- [ ] `aksl_fwrite` happy path; write to a read-only stream → `AKERR_IO`; write to a full
device (`/dev/full`) → `ENOSPC`/`AKERR_IO`; `fp == NULL``AKERR_NULLPOINTER`.
- [ ] `aksl_fclose` happy path; `NULL``AKERR_NULLPOINTER`; double-close is caught or
documented as UB.
- [ ] `aksl_fclose` on a stream whose buffered flush fails (`/dev/full`) → non-zero `fclose`
surfaces `errno`.
- [ ] Round-trip test: `fopen``fwrite``fclose``fopen``fread` → compare bytes.
### 1.3 Formatted output
- [ ] `aksl_printf` / `aksl_fprintf` / `aksl_sprintf` happy paths, asserting both the byte
count written through `count` and the produced text.
- [ ] Each of the three with every pointer argument NULL in turn → `AKERR_NULLPOINTER`.
- [ ] `aksl_fprintf` to a closed / read-only stream → error path, and confirm `*count` is not
left holding `-1` as if it were a valid length.
- [ ] `aksl_sprintf` with a format that overflows the destination — currently unbounded
(§2.2.4). Test the `aksl_snprintf` replacement once it exists.
- [ ] Regression test for the missing `va_end` (§2.1.4) — a test that calls each variadic
wrapper many times in a loop, run under valgrind/ASan.
- [ ] Format-string/arg mismatch is caught at compile time once
`__attribute__((format(printf, ...)))` is added (§2.2.5) — a negative compile test.
### 1.4 String → number
- [ ] `aksl_atoi` / `atol` / `atoll` / `atof` happy paths, including negative values and
leading whitespace.
- [ ] NULL `nptr` and NULL `dest` for each → `AKERR_NULLPOINTER`.
- [ ] **Non-numeric input** (`"not a number"`) — **[CONFIRMED]** currently returns *success*
with `*dest == 0`. Test the `AKERR_VALUE` behaviour once §2.1.5 is fixed.
- [ ] **Overflow** (`"99999999999999999999"`) — **[CONFIRMED]** currently returns success with
a garbage value (`-1` on this box). Test for `ERANGE`.
- [ ] Empty string, `" "`, `"12abc"` (trailing junk), `"0x10"`, `"inf"`/`"nan"` for `atof`.
- [ ] `LONG_MIN`/`LONG_MAX`/`LLONG_MIN`/`LLONG_MAX` boundary strings round-trip exactly.
### 1.5 `aksl_realpath`
- [ ] Happy path on an existing file and on a symlink chain; result matches `realpath(3)`.
- [ ] Non-existent path → `ENOENT`.
- [ ] A path component that is not a directory → `ENOTDIR`.
- [ ] Symlink loop → `ELOOP`.
- [ ] `path == NULL``AKERR_NULLPOINTER`.
- [ ] `resolved_path == NULL` — currently unchecked and leaks (§2.1.6). Test once fixed.
- [ ] A failure case where `resolved_path` is an *uninitialised* buffer — this is the crash
case in §2.1.6; run it under ASan/MSan.
### 1.6 `aksl_strhash_djb2`
- [ ] Known-answer vectors: `djb2("")` == 5381; a handful of fixed strings with their
pre-computed 32-bit values.
- [ ] `len == 0` returns 5381 regardless of `str` contents.
- [ ] NULL `str` and NULL `hashval``AKERR_NULLPOINTER`.
- [ ] **High-bit bytes** (`"\xff\xfe"`) — pins down the sign-extension bug in §2.2.6; the
expected value must be the `unsigned char` one.
- [ ] Embedded NUL bytes are hashed (the function is length-driven, not NUL-driven).
- [ ] Same input → same output across two calls (no hidden state).
### 1.7 Linked list
- [ ] **`aksl_list_append` builds a correct chain of N nodes** — **[CONFIRMED BROKEN]**, see
§2.1.1. Appending `n1..n4` to `n0` yields the chain `n0 -> n4`; `n1`, `n2`, `n3` are
silently dropped. This is the single most important test to add.
- [ ] `append` sets `obj->prev` to the real tail and `obj->next` to NULL.
- [ ] `append` onto an empty (single, zeroed) node.
- [ ] `append` NULL list / NULL obj → `AKERR_NULLPOINTER`.
- [ ] `append` onto a list containing a cycle → `AKERR_CIRCULAR_REFERENCE`, for a self-loop,
a 2-node cycle, and a cycle that does not include the head.
- [ ] `append` a node that is already in the list (aliasing) — define and test the contract.
- [ ] **`aksl_list_iterate` visits every node exactly once, starting at the head** —
**[CONFIRMED BROKEN]**, see §2.1.2: it starts iterating from the *midpoint* left behind
by the cycle detector, so the head is never visited. Assert both the visit count and
the visit order.
- [ ] `iterate` over a single-node list visits exactly one node.
- [ ] `iterate` NULL list / NULL iter → `AKERR_NULLPOINTER`.
- [ ] `iterate` over a cyclic list → `AKERR_CIRCULAR_REFERENCE` (self-loop, 2-node,
tail-to-middle).
- [ ] `iterate` where the callback raises `AKERR_ITERATOR_BREAK` → iteration stops at that
node, the wrapper returns success, and the visit count proves the early exit.
- [ ] `iterate` where the callback raises some *other* error → that error propagates out
unchanged, with the callback's message intact.
- [ ] `aksl_list_pop` on a middle node relinks `prev`/`next` correctly and clears the popped
node's pointers.
- [ ] `pop` on the head node, on the tail node, and on a single-node list.
- [ ] `pop(NULL)``AKERR_NULLPOINTER`.
- [ ] `pop` then `iterate` — the list is still traversable and the popped node is gone.
- [ ] Pool-accounting test: after a long sequence of list operations including failures,
`akerr_slots_in_use() == 0`.
### 1.8 Tree
- [ ] Visit **order** assertions, not just counts, for `DFS_PREORDER`, `DFS_INORDER` and
`DFS_POSTORDER` over the 7-node tree — record the visited node pointers into an array
and compare against the expected sequence. The current test only counts steps, which
cannot distinguish the three orders (all three visit 7 nodes).
- [ ] **`AKERR_ITERATOR_BREAK` aborts the whole traversal** — **[CONFIRMED BROKEN]**, see
§2.1.3. The pre-order case exists as `tests/test_tree_iterate_break.c`: pre-order
visits 0, 1, 3, so breaking at `tree[3]` must stop the walk at **3** visits, and today
it runs on to all 7. Add the equivalent for in-order (3, 1, 4, 0, 5, 2, 6 → breaking at
`tree[4]` gives 3) and post-order (3, 4, 1, 5, 6, 2, 0 → breaking at `tree[5]` gives 4).
- [ ] `AKSL_TREE_SEARCH_BFS` and `AKSL_TREE_SEARCH_BFS_RIGHT``AKERR_NOT_IMPLEMENTED`
today; replace with real order assertions once implemented (§3 / §2.2.9).
- [ ] **Unknown `searchmode`** (e.g. `99`) — **[CONFIRMED]** currently returns *success*
having visited nothing. Should be `AKERR_VALUE`.
- [ ] **`AKSL_TREE_SEARCH_VISIT`** (defined in the header, `5`) — **[CONFIRMED]** falls into
the same silent-success hole. Either implement it or reject it.
- [ ] `root == NULL` / `iter == NULL``AKERR_NULLPOINTER`.
- [ ] Single-node tree (no children) visits exactly once, in all three orders.
- [ ] Left-only and right-only degenerate chains.
- [ ] Callback raising a non-`ITERATOR_BREAK` error propagates out of the recursion with the
original status and message.
- [ ] Custom `lalloc`/`lfree` are actually invoked — **currently they are stored and never
called** (§2.2.8), so this test will fail until BFS lands.
- [ ] Deep/degenerate tree (e.g. 100k-node left chain) — documents the recursion-depth limit
(§2.2.7).
- [ ] A tree containing a cycle (child pointing back at an ancestor) — currently infinite
recursion; test for `AKERR_CIRCULAR_REFERENCE` once guarded.
### 1.9 Cross-cutting
- [ ] **Error-pool accounting**: for *every* wrapper, a test that drives the failure path
`AKERR_MAX_ARRAY_ERROR + 10` times and asserts the pool does not leak
(`akerr_slots_in_use() == 0` after each handled error).
- [ ] **Stack-trace content**: assert that the file/function/line recorded by each wrapper's
`FAIL` points at `src/stdlib.c` and the right function name.
- [ ] **`AKERR_NOIGNORE` is effective**: a negative compile test where a wrapper's return is
discarded and `-Werror=unused-result` fires.
- [ ] **Thread safety**: libakerror's `AKERR_ARRAY_ERROR` is a process-global array with no
locking. If `libakstdlib` is meant to be callable from threads, add a concurrent
smoke test under TSan — and if it is not, say so in the README.
---
## 2. Corner cases in existing functionality that should be resolved
### 2.1 Confirmed defects (reproduced against the built library)
The first three now have a failing test apiece, registered in `AKSL_KNOWN_FAILING_TESTS`
and therefore marked `WILL_FAIL` so the suite stays green while the gap stays visible:
`tests/test_list_append_chain.c`, `tests/test_list_iterate_head.c` and
`tests/test_tree_iterate_break.c`. When one of these defects is fixed, CTest reports that
test as failed with *unexpectedly passed* — that is the cue to move it into `AKSL_TESTS`.
1. **`aksl_list_append` does not find the tail — it silently truncates the list.**
`src/stdlib.c:194`. The function conflates Floyd cycle detection with tail-finding:
`tail` is set to `slow` *before* `slow` advances, so it tracks the node *behind the
midpoint*, not the tail. Appending `n1`, `n2`, `n3`, `n4` to `n0` produces the chain
`n0 -> n4`; `n1``n3` are unlinked and lost. The fix is to keep the cycle check but walk
a separate cursor to the real tail (`while (tail->next) tail = tail->next;`), or run
Floyd first and then walk to the end.
2. **`aksl_list_iterate` skips the first half of the list.** `src/stdlib.c:324`. After the
cycle-detection loop, `slow` is left at the list midpoint, and the visiting loop then
starts from `slow` instead of from `list`. The head node is never passed to the callback.
Fix: iterate from `list`, not from `slow`.
3. **`AKERR_ITERATOR_BREAK` does not stop a tree traversal.** `src/stdlib.c:251`. The
recursive frame in which the callback raises the break handles it in its own
`PROCESS`/`HANDLE(e, AKERR_ITERATOR_BREAK)` block and returns *success*; the parent's
`PASS` therefore sees no error and continues on to the sibling subtree. Breaking at
`tree[3]` in pre-order still visits all 7 nodes. Fix by splitting the recursion: an
internal helper that propagates `AKERR_ITERATOR_BREAK` unhandled, and a public entry point
that swallows it exactly once at the top. `tests/test_tree.c` currently passes only
because the search target is the last node in all three orders.
4. **`va_end` is never called.** `src/stdlib.c:96`, `:109`, `:122` each call `va_start` with
no matching `va_end` on any path — happy or error. This is undefined behaviour per the C
standard and leaks register-save state on some ABIs.
5. **`aksl_atoi`/`atol`/`atoll`/`atof` cannot report a conversion failure.**
`src/stdlib.c:135``169`. `atoi("not a number")` returns success with `0`;
`atoi("99999999999999999999")` returns success with a wrapped value. Since the entire
point of this library is turning silent libc failures into error contexts, these should be
reimplemented over `strtol`/`strtoll`/`strtod` with `errno = 0` before the call, an
`endptr` check for "no digits consumed" and "trailing junk", and a range check —
raising `AKERR_VALUE` and `ERANGE` respectively. Keep the `atoi`-compatible names but
document the stricter contract, or add `aksl_strtol`-family wrappers alongside.
6. **`aksl_realpath` mishandles `resolved_path`.** `src/stdlib.c:171`.
- `resolved_path` is never NULL-checked. `realpath(path, NULL)` is valid and mallocs a
buffer, but the wrapper discards `result`, so the caller gets nothing and the buffer
leaks.
- On failure the message formats `resolved_path` with `%s` while `realpath` leaves it
unspecified — for the normal caller who passed an uninitialised stack buffer, the error
path itself reads uninitialised memory and can crash.
- There is no way to express the buffer's size, so callers must know to supply `PATH_MAX`
bytes. Consider `aksl_realpath(const char *path, char *buf, size_t buflen)`, or an
allocating variant that returns the malloc'd pointer through an out-param.
### 2.2 Latent issues and API contract gaps
1. **`errno` is used as an error status where it may be stale.** `aksl_malloc` reports
`errno` when `malloc` returns NULL, but `malloc(0)` may legitimately return NULL without
setting `errno`, producing a `FAIL` with `status == 0` — an error context that every
`DETECT`/`CATCH` will read as *success* while still holding a pool slot. Guard every
`errno`-sourced status with a fallback (`errno ? errno : AKERR_IO`) and set `errno = 0`
before the call being wrapped.
2. **`aksl_fopen` does not validate `pathname` or `mode`.** `fopen(NULL, ...)` is UB. Add
`AKERR_NULLPOINTER` guards.
3. **`aksl_fread`/`aksl_fwrite` lose the transfer count and hide short transfers.**
- `ptr` is never NULL-checked in either function.
- Neither has an out-param for the number of members actually transferred, so a caller who
gets `AKERR_EOF` cannot tell how much data arrived. Add `size_t *nmemb_out`.
- If `nmemr != nmemb` but neither `feof` nor `ferror` is set, both functions fall through
to `SUCCEED_RETURN` — a short transfer reported as complete success.
- `aksl_fwrite`'s error message reads `"Error reading file"` (`src/stdlib.c:83`), and
checking `feof()` on a write path is meaningless.
4. **`aksl_sprintf` wraps `vsprintf`, which is unbounded.** There is no way for a caller to
bound the destination. Add `aksl_snprintf`/`aksl_vsnprintf` and consider deprecating the
`sprintf` wrapper, or reject it outright — an "error-handling" wrapper around an
unbounded write is a sharp edge the library exists to remove.
5. **No `format` attribute on the variadic wrappers.** Adding
`__attribute__((format(printf, 2, 3)))` (and the equivalents) restores the compile-time
format/argument checking that callers lose by going through the wrapper.
6. **`aksl_strhash_djb2` sign-extends bytes ≥ 0x80.** `src/stdlib.c:181` iterates a
`char *`, which is signed on x86/ARM Linux, so high bytes contribute a sign-extended
negative value and the hash differs from the canonical djb2 — and differs across
platforms where `char` is unsigned. Iterate a `const unsigned char *`. Also take
`const char *` in the signature so callers need not cast away constness.
7. **`aksl_tree_iterate` recursion is unbounded and cycle-blind.** A deep or degenerate tree
overflows the stack, and a child pointer that loops back to an ancestor recurses forever.
Add a depth limit (raising `AKERR_OUTOFBOUNDS`) and/or a visited check raising
`AKERR_CIRCULAR_REFERENCE`, consistent with what the list functions already do.
8. **`lalloc`, `lfree` and `queue` are dead parameters.** `lalloc`/`lfree` are defaulted and
then never called; `queue` is entirely unused (it is the only `-Wunused-parameter` warning
in the file). The doc comment even tells the caller to "pass NULL here", which is a sign
the queue belongs in an internal helper rather than the public signature. Either
implement BFS so they are used, or drop them from the public API until it is.
9. **Unknown `searchmode` values return success.** The `switch` in `aksl_tree_iterate` has no
`default:`; `searchmode == 99` and the header-defined `AKSL_TREE_SEARCH_VISIT` (5) both
fall straight through to `SUCCEED_RETURN` having visited nothing. Add
`default: FAIL_RETURN(e, AKERR_VALUE, ...)`.
10. **`AKSL_TREE_SEARCH_BFS`/`BFS_RIGHT` are unimplemented** (`AKERR_NOT_IMPLEMENTED`), and
`AKSL_TREE_SEARCH_VISIT` is declared in the header with a documented meaning but no
implementation anywhere.
11. **`aksl_memset`'s and `aksl_memcpy`'s inner checks are dead code.** `memset` returns `s`
and `memcpy` returns `d`, both unconditionally; neither can fail. The
`FAIL_ZERO_RETURN(e, memset(...), errno, ...)` and `(memcpy(...) == d)` checks can never
fire and only obscure the intent. Also, `memcpy` with overlapping ranges is UB — either
document that or dispatch to `memmove`.
12. **`aksl_list_pop` cannot tell the caller the new head.** Popping the head leaves the
caller's head pointer dangling at a now-detached node. Add an out-param for the new head,
or a `aksl_list_pop(aksl_ListNode **head, aksl_ListNode *node)` form.
13. **`aksl_free` does not clear the caller's pointer**, so double-free remains easy. Consider
an `aksl_freep(void **ptr)` that frees and NULLs.
14. **No initialisers for the public structs.** Every caller must remember to `memset` an
`aksl_ListNode`/`aksl_TreeNode` to zero before use (both existing tests do). Add
`aksl_list_node_init`/`aksl_tree_node_init` or `AKSL_LIST_NODE_INIT` macros.
15. **`aksl_TreeNode.parent` is declared but never set or read** by any library function.
16. **Header hygiene**: no `extern "C" { }` guard for C++ consumers; no version macros
(`AKSL_VERSION_MAJOR`…); `akstdlib.h` pulls in `stdio.h`/`stdlib.h`/`string.h`/`stdint.h`
into every consumer's namespace.
17. **Doxygen coverage is 2 functions out of 20.** A `Doxyfile` exists but only
`aksl_tree_iterate` and `aksl_list_iterate` have doc comments. Every public function
needs `@param`/`@throws`/`@return`, especially the ones whose contract deviates from libc
(`aksl_free(NULL)` is an error; `aksl_atoi` will become strict).
### 2.3 Build, CI and repository
- [ ] **`CMakeLists.txt:4-6` sets `CMAKE_CXX_FLAGS` in a C-only project**, so `-g -ggdb -pg`
never reach the C compiler. Use `CMAKE_C_FLAGS` — or better, drop `-pg` from the
default build entirely (it is what produces the stray `gmon.out` sitting untracked in
the repo root) and let `CMAKE_BUILD_TYPE` control debug info.
- [ ] **No warning flags.** Add `-Wall -Wextra` (and ideally `-Werror` in CI). The only
current warning is the unused `queue` parameter, so the cost of turning them on is low.
- [ ] `src/stdlib.c` triggers **"ISO C99 requires at least one argument for the `...`"** on
roughly 20 lines under `-Wpedantic`, because `FAIL_*` is called with a bare message and
no varargs. Either always pass an argument, or add a zero-arg-safe form in libakerror.
- [ ] **The `deps/libakerror` submodule is pinned 11 commits behind `main`** (pinned at
`4fad0ce "Add gitea workflow"`; upstream is at `4212ff0`). The pin predates the
refcount-leak fix, the stack-trace buffer-overflow fix, the `AKERR_MAX_ERR_VALUE`
correction and the format-string fix. Bump it.
- [ ] **CI does not build against the submodule it pins.** `.gitea/workflows/ci.yaml` clones
`libakerror@main` and installs it, while the build it then runs is top-level and so
compiles `deps/libakerror` at the pinned commit — two different libakerror versions
depending on where you build, and the installed one is never actually linked. Pick
one. (The submodule is at least *present* in CI now: §1.0 added
`submodules: recursive` to the checkout, without which configure failed outright.)
- [x] **`ctest` was red** (`tree` failing plus four *Not Run* submodule tests) — fixed in
§1.0. The build badge in `README.md` means something again.
- [ ] **Untracked build litter** in the repo root: `build/`, `gmon.out`,
`CMakeLists.txt-akstdlib`, and a dozen `*~` editor backups. Add a `.gitignore`
(libakerror has one; this repo does not).
- [ ] **`README.md` is a build badge and nothing else.** It needs at minimum: what the
library is, the akerror wrapper contract, the list of wrapped functions, and the
deviations from libc semantics.
- [ ] **Indentation is inconsistent**`src/stdlib.c` mixes hard tabs and 4-space indents
within the same functions. libakerror standardised on Stroustrup style
(commit `e5f7616`); apply the same here, ideally with a checked-in `.clang-format`.
---
## 3. libc functions not yet wrapped
Ordered roughly by how much a caller of this library would miss them. Each entry means "add
an `aksl_`-prefixed wrapper that turns the documented failure modes into an
`akerr_ErrorContext`".
### 3.1 High priority — gaps in areas the library already covers
**Memory (`stdlib.h`, `string.h`)**
- [ ] `calloc`, `realloc`, `reallocarray``realloc`'s "returns NULL and the old pointer is
still valid" trap is exactly what this library should be hiding.
- [ ] `aligned_alloc`, `posix_memalign`
- [ ] `memmove`, `memcmp`, `memchr`
**Bounded string formatting (`stdio.h`)**
- [ ] `snprintf`, `vsnprintf` — needed to make §2.2.4 fixable.
- [ ] `vprintf`, `vfprintf`, `vsprintf` — the `va_list` forms, so consumers can build their
own variadic wrappers on top.
- [ ] `asprintf` / `vasprintf` (GNU) as an allocating alternative.
**String → number (`stdlib.h`)**
- [ ] `strtol`, `strtoll`, `strtoul`, `strtoull`, `strtod`, `strtof`, `strtold` — the correct
foundation for §2.1.5.
**Strings (`string.h`)**
- [ ] `strlen`, `strnlen`
- [ ] `strcpy`, `strncpy`, `strcat`, `strncat` — with truncation reported as an error rather
than silently accepted, which is the whole value proposition here.
- [ ] `strdup`, `strndup`
- [ ] `strcmp`, `strncmp`, `strcasecmp`, `strncasecmp`, `strcoll`
- [ ] `strchr`, `strrchr`, `strstr`, `strcasestr`, `strpbrk`, `strspn`, `strcspn`
- [ ] `strtok_r`, `strsep`
- [ ] `strerror_r`
**Stream I/O (`stdio.h`)**
- [ ] `fseek`, `ftell`, `rewind`, `fgetpos`, `fsetpos`, `fseeko`, `ftello`
- [ ] `fflush`
- [ ] `fgets`, `fputs`, `fgetc`/`getc`/`getchar`, `fputc`/`putc`/`putchar`, `ungetc`
- [ ] `getline`, `getdelim`
- [ ] `freopen`, `fdopen`, `fileno`
- [ ] `setvbuf`, `setbuf`
- [ ] `clearerr`, `feof`, `ferror` — thin, but worth exposing so callers never touch raw
`FILE *` internals.
- [ ] `sscanf`, `fscanf`, `scanf` — the return-value semantics (items matched vs `EOF`) are a
classic silent-failure source.
- [ ] `remove`, `rename`, `tmpfile`, `mkstemp`, `mkdtemp`
- [ ] `perror` — or an akerror-native equivalent.
### 3.2 POSIX file and process API (likely the next major surface)
**`unistd.h` / `fcntl.h`**
- [ ] `open`, `close`, `read`, `write`, `pread`, `pwrite`, `lseek`
- [ ] `readv`, `writev`
- [ ] `dup`, `dup2`, `pipe`, `fcntl`
- [ ] `fsync`, `fdatasync`, `truncate`, `ftruncate`
- [ ] `unlink`, `link`, `symlink`, `readlink`, `rmdir`, `mkdir`
- [ ] `access`, `faccessat`, `chmod`, `fchmod`, `chown`, `fchown`, `umask`
- [ ] `chdir`, `fchdir`, `getcwd`
- [ ] `isatty`, `ttyname_r`
- [ ] `sysconf`, `pathconf`
- [ ] `sleep`, `usleep`, `nanosleep`
**`sys/stat.h`**
- [ ] `stat`, `fstat`, `lstat`, `fstatat`
- [ ] `statvfs`, `fstatvfs`
**`dirent.h`**
- [ ] `opendir`, `fdopendir`, `readdir`, `readdir_r`, `closedir`, `rewinddir`, `scandir`
**Process control**
- [ ] `fork`, `execve` / `execvp` / `execl` family, `waitpid`, `wait`
- [ ] `posix_spawn`
- [ ] `system`, `popen`, `pclose`
- [ ] `getpid`, `getppid`, `getuid`, `geteuid`, `setuid`, `setgid`
- [ ] `atexit`, `exit`, `_exit`, `abort` — mostly to give akerror a hook on shutdown.
- [ ] `getenv`, `setenv`, `unsetenv`, `putenv`, `clearenv`
**`sys/mman.h`**
- [ ] `mmap`, `munmap`, `mprotect`, `msync`, `madvise`
### 3.3 Time
- [ ] `time`, `clock_gettime`, `clock_getres`, `gettimeofday`
- [ ] `localtime_r`, `gmtime_r`, `mktime`, `timegm`, `difftime`
- [ ] `strftime`, `strptime`
- [ ] `clock`, `times`
### 3.4 Sorting, searching and misc `stdlib.h`
- [ ] `qsort`, `qsort_r`, `bsearch` — comparator errors currently have nowhere to go; an
akerror-aware comparator signature would be a genuine improvement.
- [ ] `abs`, `labs`, `llabs`, `div`, `ldiv`, `lldiv`
- [ ] `rand`, `srand`, `random`, `srandom`, `getrandom`/`arc4random`
### 3.5 Lower priority / larger projects
- [ ] **Sockets**: `socket`, `bind`, `listen`, `accept`, `connect`, `send`/`sendto`/`sendmsg`,
`recv`/`recvfrom`/`recvmsg`, `shutdown`, `setsockopt`/`getsockopt`, `getaddrinfo`/
`freeaddrinfo`/`gai_strerror`, `inet_ntop`/`inet_pton`
- [ ] **Multiplexing**: `select`, `poll`, `ppoll`, `epoll_create1`/`epoll_ctl`/`epoll_wait`
- [ ] **Signals**: `sigaction`, `sigprocmask`, `sigemptyset`/`sigaddset`, `kill`, `raise`,
`signalfd`
- [ ] **Threads**: `pthread_create`/`join`/`detach`, `pthread_mutex_*`, `pthread_cond_*`,
`pthread_rwlock_*`, `sem_*` — blocked on resolving the thread-safety question in §1.9
(libakerror's error pool is an unlocked process-global array).
- [ ] **Dynamic loading**: `dlopen`, `dlsym`, `dlclose`, `dlerror`
- [ ] **Locale / wide chars**: `setlocale`, `mbstowcs`, `wcstombs`, `iconv_*`
- [ ] **Math**: the `math.h` functions that set `errno`/raise FP exceptions (`sqrt`, `log`,
`pow`, `acos`, …) — probably better served by a dedicated `libakmath`.
### 3.6 Non-libc additions the current data structures imply
Not libc wrappers, but the existing list/tree API is visibly incomplete:
- [ ] List: `prepend`, `insert_after`/`insert_before`, `length`, `find`, `reverse`,
`concat`, `free_all`, reverse iteration, and a head/tail-tracking container type so
`append` is O(1) instead of O(n).
- [ ] Tree: `insert`, `remove`, `find`, `height`, `count`, `free_all`, and the BFS traversal
the `lalloc`/`lfree`/`queue` parameters were designed for.
- [ ] Hash map built on `aksl_strhash_djb2`, plus additional hashes (FNV-1a, xxHash) and a
NUL-terminated `aksl_strhash_djb2_str` convenience form.
- [ ] Growable buffer / string-builder type, to make the `snprintf` and `strcat` wrappers
pleasant to use.
# Record
**Outstanding work is in the issue tracker, not in this file:**
<https://source.starfort.tech/andrew/libakstdlib/issues>
What stays here is what a tracker has no place for: where the library stands, and
the decisions that would otherwise be re-litigated — what is deliberately *not*
wrapped, and which uncovered lines are uncoverable rather than untested.
Issues are labelled by kind and blast radius, and milestoned by what they can land
in: `0.2.x` for anything that breaks no ABI, `0.3.0` for new public symbols,
`1.0.0` for the large surfaces. Everything filed carries `status::grooming` until
it has been through grooming.
## Where the library stands
| | |
|---|---|
| Wrapped | 154 public functions across `src/stdlib.c`, `src/string.c`, `src/stream.c`, `src/collections.c` |
| Tests | 17 CTest binaries plus 2 negative-compile entries, green under the default and sanitizer builds |
| Line coverage | 99.5% (1708/1716) |
| Function coverage | 100% (154/154) |
| Doxygen | 100% of 154, gated — `cmake --build build --target docs` fails on an undocumented function, parameter or return |
| Mutation score | 72.3% (188/260 sampled from 1701), gated at 65 |
The six confirmed defects that used to head this file are fixed and
`AKSL_KNOWN_FAILING_TESTS` is empty. What they were, and what changed as a result,
is in `UPGRADING.md`.
## What libakerror costs this library
Three of these are not fixable from inside this repository, and each costs
something here. They are filed in both places, because the fix is there and the
bill is here. The fourth turned out not to be blocked at all:
| Here | Upstream | What it costs |
|---|---|---|
| #2 | — | **Corrected while filing.** The unlocked error pool is a property of the libakerror this repository *pins* (1.0.0), not of libakerror (2.0.1, which locks it). `libakgl` and `akbasic` are both on 2.0.1. The work is a submodule bump and the verification that goes with it, not a wait |
| #3 | libakerror | `IGNORE()` leaks a context, so `aksl_tree_iterate` open-codes log-then-release in four lines that should be one |
| #4 | libakerror | The `coverage` target is not namespaced when embedded, so `-DAKSL_COVERAGE=ON` fails to configure and `CMakeLists.txt` shadows `add_custom_target` to work around it |
| #5 | libakerror | No `akerrorConfigVersion.cmake`, so `find_dependency(akerror)` cannot ask for the 1.0.0 floor |
## Uncovered lines that are uncoverable
Eight lines, and this is what they are, so the coverage listing does not read as an
oversight.
**Two are the short-transfer branch** in `aksl_fread`/`aksl_fwrite` — a short
transfer with neither EOF nor a stream error. The standard permits it, so the
branch is correct to have; every way of actually producing one on Linux sets `feof`
or `ferror` first. **It is the only error path in the library that has never
executed**, and reaching it needs a `FILE *` over a custom stream (`fopencookie`,
`funopen`). That is #6.
**Two are the string-buffer overflow guard** — the `capacity = needed` arm in
`strbuf_reserve`. Reaching it needs an `aksl_StrBuf` within a factor of two of
`SIZE_MAX`, **which is not a test, it is a hang.** It is there because doubling a
capacity is a multiplication, and an unguarded one is how a growable buffer turns
into a heap overflow. Nothing to do.
**Four are `HANDLE(e, AKERR_ITERATOR_BREAK)` lines**, and they are macro artifacts
rather than gaps. In libakerror that macro begins with the `break;` belonging to
`PROCESS`'s `case 0:` arm, reachable only when a callback returns a non-NULL context
whose status is *zero* — the pathological case the errno-fallback work removed. Left
uncovered deliberately rather than pinned by a test that would have to manufacture
it.
## `aksl_version_check()` ignores its `patch` argument
`src/stdlib.c`, the `(void)patch`. **Correct for the current "same soname" rule**
patch level never breaks the ABI — but the parameter exists only so the error
message can name the caller's full version.
No consequence today. If a future compatibility rule needs the patch level to
participate, that is the line to change, and the `#if` in `tests/test_version.c` is
the test that encodes the rule.
## Deliberate omissions
Recorded so nobody adds them thinking they were forgotten. **Each is a decision,
and each can be revisited with an argument.**
| Not wrapped | Why |
|---|---|
| `sprintf`, `vsprintf` | Cannot be bounded. An error-handling wrapper around an unbounded write is the sharp edge this library exists to remove. `aksl_snprintf` and `aksl_asprintf` cover both real uses. |
| `strtok` | Keeps its state in a hidden static, so two interleaved tokenisations corrupt each other silently and any use from a thread is a bug. `aksl_strtok_r` and `aksl_strsep` cover it. |
| `strcpy`, `strcat` as libc spells them | Cannot be called safely without the destination's size. The wrappers take it. |
| `setbuf` | Exactly `setvbuf(stream, buf, buf ? _IOFBF : _IONBF, BUFSIZ)` and strictly less expressive. Wrapping it would add a second way to say one thing. |
| `perror` | Writes to stderr and consults a global. `aksl_strerror` is the akerror-native equivalent and knows this library's own statuses as well as errno's. |
| `strerror_r` | Two incompatible functions share that name and which one you get depends on feature-test macros a consumer cannot influence from inside this header. `aksl_strerror` is built on libakerror's registry instead. |
## What the mutation survivors mean
The harness samples 260 of 1701 mutants and kills 72.3%. **Most of the 72 survivors
are equivalent mutants rather than missing tests** — `README.md` has the full
breakdown — and knowing which is which is the point, because a ratchet built on the
wrong number is a ratchet that stops moving.
Three clusters are real work and are #7. Two survivors in that class were real and
are fixed: the right child's `depth + 1` in the depth-first walk, and
`aksl_tree_remove` on an empty tree.
## Evidence from the first full consumer
`akbasic` (`source.starfort.tech/andrew/akbasic`) is a ~6,300-line C interpreter
built on this library and `libakerror`. It was the first consumer to exercise the
whole surface rather than a corner of it, **and what it could not use is what
prioritised everything that has been built since.**
**The number that started it.** Across `src/`, akbasic made **10 calls into this
library and 116 to raw libc** — a library whose value proposition is "turn silent
libc failures into error contexts", bypassed 92% of the time by the consumer most
committed to it.
| Raw libc it had to use | Count | Now available as |
|---|---|---|
| `strlen` | 37 | `aksl_strlen` |
| `snprintf` | 28 | `aksl_snprintf` |
| `strcmp` | 16 | `aksl_strcmp` |
| `memcpy` / `memset` | 16 | `aksl_memcpy` / `aksl_memset` |
| `strncpy` | 15 | `aksl_strncpy` |
| `strtoll` / `strtod` | 2 | `aksl_strtoll` / `aksl_strtod` |
| `fgets` | 2 | `aksl_fgets` |
| `strstr` | 1 | `aksl_strstr` |
**All four things the port had to write for itself now exist here.**
1. **A strict `strtoll`/`strtod` wrapper** (`akbasic/src/convert.c`, ~60 lines). The
`aksl_strto*` family is that, with the endptr/`errno`/range contract. akbasic
formally banned the `aksl_ato*` family because routing four diagnosable errors
through it would have turned them into wrong answers — `VAL("garbage")` silently
returning `0.0`. **That ban can be lifted**: the `ato*` forms report failures now.
2. **A fixed-capacity string-keyed hash table** (`akbasic/src/symtab.c`, ~130 lines,
needed three times over). `aksl_hashmap_*` is that table generalised, **with
tombstones on delete, which the original did not have.**
3. **The bounded-copy-with-truncation-as-error idiom, at ten sites.** `aksl_strcpy`
and `aksl_strncpy` are exactly that idiom.
4. **Uppercase folding for case-insensitive lookup, three times.** `aksl_strcasecmp`
and `aksl_strncasecmp`.
**And the four confirmed-with-impact defects are closed.** The unbounded
`aksl_sprintf` is gone; `aksl_fopen`'s arguments are checked, so
`akbasic_cmd_dload`'s hand-rolled validation and its comment pointing here can go;
the sign-extended djb2 reads bytes unsigned; and the missing `va_end` — which
akbasic's stdio text sink ran on every line of program output — is fixed.
**Still true, and still shaping the wishlist.** akbasic uses no allocator, no lists
and no trees, drawing everything from fixed pools by design. **A consumer that does
allocate would weight the `open`/`read`/`write` work far higher than this one
does**, so one consumer's count is evidence, not a plan. Re-counting against this
release is #26.

178
UPGRADING.md Normal file
View File

@@ -0,0 +1,178 @@
# Upgrading
## 0.1.0 → 0.2.0
This is an ABI break and a source break. Pre-1.0 the soname is `MAJOR.MINOR`, so
`libakstdlib.so.0.1` and `libakstdlib.so.0.2` are different libraries as far as
the loader is concerned and a consumer built against 0.1 will not silently pick
this up. Rebuild against the new header.
`AKSL_VERSION_CHECK()` catches the pairing at runtime if a stale `.so` ever does
end up on the path.
Everything below was recorded in `TODO.md` before the move to the tracker — the confirmed
defects, all of which were reproduced against the 0.1.0 library before being
fixed.
### Behaviour changes with the same signature
**`aksl_atoi`, `aksl_atol`, `aksl_atoll`, `aksl_atof` report bad conversions.**
This is the one to look at first, because the compiler will not tell you about
it. They used to return success for anything: `aksl_atoi("not a number", &n)`
gave you `0`, and `aksl_atoi("99999999999999999999", &n)` gave you a wrapped
value. Now:
| input | before | after |
|---|---|---|
| `"42"` | success, 42 | success, 42 |
| `"not a number"` | **success, 0** | `AKERR_VALUE` |
| `""` or `" "` | **success, 0** | `AKERR_VALUE` |
| `"12abc"` | **success, 12** | `AKERR_VALUE` |
| `"0x10"` | **success, 0** | `AKERR_VALUE` (base 10 stops at the `x`) |
| `"99999999999999999999"` | **success, garbage** | `ERANGE` |
`*dest` is `0` on every failure path. If you were relying on the old silence —
treating an unparseable string as zero on purpose — that is now an error you
have to handle or deliberately ignore.
For `"0x10"` and friends, use `aksl_strtol(nptr, NULL, 0, &dest)`, which honours
the `0x` and `0` prefixes. The whole `aksl_strto*` family is new and is what the
`ato*` forms are built on.
**Errors no longer carry status `0`.** Any wrapper that reported `errno` could
previously raise an error whose status was whatever `errno` happened to hold,
including `0` — which every `DETECT` and `CATCH` downstream reads as *success*
while the context still holds a pool slot. `errno` is now cleared before each
wrapped call and read back through a fallback. If you were matching on a
specific status, check it is still the one you get; several calls that used to
report a flat `AKERR_IO` now report the real `errno` (a read from a write-only
stream is `EBADF`, not `AKERR_IO`).
**`aksl_malloc(0, &p)` is `AKERR_VALUE`.** It used to depend on whether the
platform's `malloc(0)` returned NULL.
**`aksl_memcpy` refuses overlapping ranges** with `AKERR_VALUE`. Use
`aksl_memmove`.
**`aksl_list_append` refuses a node already in the list** with `AKERR_VALUE`.
It used to relink it and orphan everything between its old position and the tail.
**`aksl_list_append` and `aksl_list_iterate` are correct now.** If you worked
around either defect — appending one node at a time and fixing up the links by
hand, or starting your own iteration from the head because the callback never
saw it — that workaround is now wrong. `append` truncated any list of two or
more nodes; `iterate` skipped everything before the list midpoint.
**`AKERR_ITERATOR_BREAK` stops a tree traversal.** It previously did not: the
walk ran to completion regardless. Code that raised it and relied on the
traversal continuing anyway (unlikely, but it was the behaviour) will now stop.
**Tree traversal is bounded.** A tree deeper than `AKSL_TREE_MAX_DEPTH` (256) is
`AKERR_OUTOFBOUNDS`, and a depth-first walk over a tree whose child points back
at an ancestor is `AKERR_CIRCULAR_REFERENCE`. Both used to run until the process
died.
**An unrecognised `searchmode` is `AKERR_VALUE`.** It used to return success
having visited nothing.
### Signature changes
**`aksl_sprintf` is gone.** It wrapped `vsprintf`, which cannot be bounded.
```c
/* before */
aksl_sprintf(&count, buf, "%s=%d", key, value);
/* after */
aksl_snprintf(&count, buf, sizeof(buf), "%s=%d", key, value);
```
Truncation is `AKERR_OUTOFBOUNDS` rather than a short success, and `*count` is
`0` on any failure rather than `vsprintf`'s `-1`.
**`aksl_realpath` takes the destination's length.**
```c
/* before */
char resolved[PATH_MAX];
aksl_realpath(path, resolved);
/* after */
char resolved[PATH_MAX];
aksl_realpath(path, resolved, sizeof(resolved));
```
`buflen` must be at least `PATH_MAX`; `realpath(3)` cannot be bounded below it,
so a shorter buffer is `AKERR_OUTOFBOUNDS` rather than an overflow. If you have
no `PATH_MAX`-sized buffer to hand, `aksl_realpath_alloc(path, &dest)` allocates
one and hands it over — release it with `aksl_free`.
**`aksl_fread` and `aksl_fwrite` take a transferred-count out-param.**
```c
/* before */
aksl_fread(buf, 1, sizeof(buf), fp);
/* after */
size_t got = 0;
aksl_fread(buf, 1, sizeof(buf), fp, &got);
```
It is required, not optional, because it is the only way a caller who gets
`AKERR_EOF` can find out how much data arrived. It is written on every path
including the failure ones. A short transfer with neither EOF nor a stream error
set is now `AKERR_IO` rather than success.
**`aksl_list_pop` takes the head by reference.**
```c
/* before */
aksl_list_pop(node);
/* after */
aksl_list_pop(&head, node); /* head is updated when node was the head */
```
Popping the head used to leave the caller's own head pointer aimed at a
now-detached node with no way to learn the new one.
**`aksl_tree_iterate` lost its `queue` parameter.**
```c
/* before */
aksl_tree_iterate(root, iter, NULL, NULL, mode, data, NULL);
/* after */
aksl_tree_iterate(root, iter, NULL, NULL, mode, data);
```
The doc comment told callers to pass `NULL`, which is a sign the queue belonged
in an internal helper. `lalloc` and `lfree` stay, and they are actually used now
— the breadth-first modes are implemented, where before they raised
`AKERR_NOT_IMPLEMENTED`.
**`aksl_strhash_djb2` takes `const char *`** and reads bytes as unsigned. Callers
no longer have to cast away constness. The hash value **changes** for any input
containing a byte ≥ 0x80: it now matches canonical djb2 instead of depending on
whether plain `char` is signed on the target. 7-bit ASCII keys are unaffected. If
you have persisted djb2 values across a restart, they will not match.
**`aksl_fopen` takes `const char *`** for both `pathname` and `mode`, and checks
them — `fopen(NULL, ...)` is undefined behaviour and used to go straight through.
**`aksl_memcpy` takes `const void *` for its source.**
### Additions
Nothing here breaks anything; see `README.md` for the full surface.
- `aksl_calloc`, `aksl_realloc`, `aksl_freep`, `aksl_memmove`, `aksl_memcmp`,
`aksl_memchr`
- `aksl_snprintf`, `aksl_vprintf`, `aksl_vfprintf`, `aksl_vsnprintf`
- `aksl_strtol`, `aksl_strtoll`, `aksl_strtoul`, `aksl_strtoull`, `aksl_strtod`,
`aksl_strtof`, `aksl_strtold`
- the whole of `src/string.c` and `src/stream.c`
- `aksl_list_prepend` / `insert_after` / `insert_before` / `length` / `find` /
`reverse` / `concat` / `free_all` / `iterate_reverse`, and the `aksl_List`
container so append is O(1)
- `aksl_tree_insert` / `find` / `remove` / `height` / `count` / `free_all`
- `aksl_hashmap_*`, `aksl_strbuf_*`, `aksl_strhash_fnv1a`
- `aksl_list_node_init` and `aksl_tree_node_init`, so a caller no longer has to
remember to `memset` a node before its first use
- the header is `extern "C"`-guarded and pulls in four standard headers rather
than six

View File

@@ -6,5 +6,11 @@ includedir=${exec_prefix}/include
Name: akstdlib
Description: C stdlib with akerror sanity wrappers
Version: @PROJECT_VERSION@
# akerror is a public dependency, not a private one: akstdlib.h includes
# akerror.h and every entry point returns an akerr_ErrorContext *, so a consumer
# compiles against both headers and links both libraries. 1.0.0 is the floor --
# it is the release that made the status registry private and gave the library an
# soname, and akstdlib.h refuses to compile against anything older.
Requires: akerror >= 1.0.0
Cflags: -I${includedir}/
Libs: -L${libdir} -lakstdlib

49
cmake/RunDoxygen.cmake Normal file
View File

@@ -0,0 +1,49 @@
# Run doxygen with PROJECT_NUMBER supplied from CMake, and fail on any warning.
#
# Driven by the `docs` target in the top-level CMakeLists.txt. It is a separate
# script rather than a COMMAND because it has to do three things in sequence --
# feed the configuration in, then read the log back, then decide -- and a
# custom-target command list cannot branch on the result of an earlier command.
#
# The version comes in from CMake rather than living in the Doxyfile so that
# project() stays the single place a version number is written.
if(NOT AKSL_DOXYGEN OR NOT AKSL_SOURCE_DIR OR NOT AKSL_VERSION)
message(FATAL_ERROR "RunDoxygen.cmake needs AKSL_DOXYGEN, AKSL_SOURCE_DIR and AKSL_VERSION")
endif()
set(_doxyfile "${AKSL_SOURCE_DIR}/Doxyfile")
set(_logfile "${AKSL_SOURCE_DIR}/doxygen-warnings.log")
file(READ "${_doxyfile}" _config)
# Later settings win in a Doxyfile, so appending is enough to override.
set(_config "${_config}\nPROJECT_NUMBER = ${AKSL_VERSION}\n")
set(_generated "${AKSL_SOURCE_DIR}/Doxyfile.generated")
file(WRITE "${_generated}" "${_config}")
execute_process(
COMMAND "${AKSL_DOXYGEN}" "${_generated}"
WORKING_DIRECTORY "${AKSL_SOURCE_DIR}"
RESULT_VARIABLE _result
OUTPUT_QUIET
)
file(REMOVE "${_generated}")
if(NOT _result EQUAL 0)
message(FATAL_ERROR "doxygen exited ${_result}")
endif()
# WARN_IF_UNDOCUMENTED and WARN_NO_PARAMDOC are on, so anything in the log is a
# public function, parameter or return value nobody has described. Treat it the
# way an uncovered line or a surviving mutant is treated: as work, not as noise.
if(EXISTS "${_logfile}")
file(READ "${_logfile}" _warnings)
string(STRIP "${_warnings}" _warnings)
if(NOT _warnings STREQUAL "")
message("${_warnings}")
message(FATAL_ERROR
"doxygen reported undocumented entities; see doxygen-warnings.log")
endif()
endif()
message(STATUS "API documentation written to ${AKSL_SOURCE_DIR}/doxygen/html")

View File

@@ -1,5 +1,17 @@
# cmake/MyLibraryConfig.cmake.in
include(CMakeFindDependencyMacro) # If your library has dependencies
# find_dependency(AnotherDependency REQUIRED) # Example dependency
# cmake/akstdlib.cmake.in -- installed as akstdlibConfig.cmake
#
# akstdlibTargets.cmake names akerror::akerror in the INTERFACE_LINK_LIBRARIES of
# the imported akstdlib target, because akstdlib links it PUBLIC. Without the
# find_dependency below, a consumer that has not already found akerror itself
# gets a bare "target not found" out of the generated targets file rather than a
# message naming the dependency it is missing.
#
# No version is requested here. libakerror installs no akerrorConfigVersion.cmake,
# so find_dependency(akerror 1.0.0) would be refused for want of a version file
# regardless of which akerror is actually installed. The 1.0.0 floor is carried by
# the `Requires: akerror >= 1.0.0` line in akstdlib.pc and enforced at compile
# time by the #error guard at the top of akstdlib.h.
include(CMakeFindDependencyMacro)
find_dependency(akerror)
include("${CMAKE_CURRENT_LIST_DIR}/akstdlibTargets.cmake")

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,37 @@
#ifndef _AKSTDLIB_VERSION_H_
#define _AKSTDLIB_VERSION_H_
/*
* GENERATED FILE -- do not edit.
*
* Configured from include/akstdlib_version.h.in by CMake. The version itself
* lives in exactly one place, the project() call in CMakeLists.txt, and flows
* from there into these macros, the shared library's SOVERSION, the Version:
* field in akstdlib.pc, and akstdlibConfigVersion.cmake. To bump the version,
* edit project(); to change what the macros look like, edit this template.
*/
#define AKSL_VERSION_MAJOR @PROJECT_VERSION_MAJOR@
#define AKSL_VERSION_MINOR @PROJECT_VERSION_MINOR@
#define AKSL_VERSION_PATCH @PROJECT_VERSION_PATCH@
#define AKSL_VERSION_STRING "@PROJECT_VERSION@"
/*
* A single ordered integer, for `#if AKSL_VERSION_NUMBER >= ...` compares.
* Computed rather than written out as a literal on purpose: a literal like
* 000100 is octal in C, which would silently make version 0.1.0 compare as 64.
* Each component gets two decimal digits, so this is correct up to x.99.99.
*/
#define AKSL_VERSION_NUMBER \
((AKSL_VERSION_MAJOR * 10000) + (AKSL_VERSION_MINOR * 100) + AKSL_VERSION_PATCH)
/*
* The soname of the library these headers describe -- "0.1" for libakstdlib.so.0.1.
* This is the granularity at which the ABI is allowed to break, and it is what
* aksl_version_check() compares: pre-1.0 a minor bump is a break, so the soname
* carries MAJOR.MINOR. From 1.0 it becomes MAJOR alone, and the SOVERSION
* expression in CMakeLists.txt and this macro change together.
*/
#define AKSL_VERSION_SONAME "@AKSL_SOVERSION@"
#endif // _AKSTDLIB_VERSION_H_

469
scripts/coverage.py Normal file
View File

@@ -0,0 +1,469 @@
#!/usr/bin/env python3
"""
Code coverage harness for libakstdlib.
Reports which lines, branches and functions of the library the CTest suite
actually executed. Coverage answers "what did the tests touch"; the mutation
harness in scripts/mutation_test.py answers the stronger question "would the
tests notice if it broke". They are complementary: a line can be covered and
still have no assertion behind it, so an uncovered line is definitely untested
while a covered one is only maybe tested.
Requires a build configured with -DAKSL_COVERAGE=ON, which compiles the library
and its tests with --coverage. That emits a .gcno next to every object at build
time and a .gcda next to it at run time; this script feeds those to gcov and
aggregates its JSON output.
There are no third-party dependencies (Python stdlib + gcc's own gcov), so this
works anywhere the project already builds -- no lcov, gcovr or genhtml needed.
Usage:
scripts/coverage.py --build build-coverage [options]
--build DIR build tree to read .gcda from (default: build-coverage)
--source-root DIR repo root (default: parent of this script's dir)
--gcov PROG gcov binary to use (default: $GCOV or gcov)
--include PREFIX only report sources under this repo-relative prefix;
repeatable. Default: src, include
--exclude PREFIX drop sources under this repo-relative prefix;
repeatable. Default: tests, deps, build
--zero delete every .gcda in the build tree, then exit. Run
before the suite so counters do not accumulate across
runs.
--run-tests run ctest in the build tree first (implies --zero)
--threshold PCT exit non-zero if total line coverage < PCT (default 0)
--branch-threshold PCT
exit non-zero if total branch coverage < PCT
--summary-only omit the per-file uncovered-line listing
--output PATH also write the text report to PATH (CTest hides the
output of a passing test, so the in-suite report lands
in <build>/coverage-summary.txt this way)
--cobertura PATH also write a Cobertura XML report (for CI publishers)
"""
import argparse
import glob
import json
import os
import subprocess
import sys
import xml.sax.saxutils as saxutils
DEFAULT_INCLUDE = ["src", "include"]
DEFAULT_EXCLUDE = ["tests", "deps", "build"]
# --------------------------------------------------------------------------- #
# gcov invocation and parsing
# --------------------------------------------------------------------------- #
class FileCoverage:
"""Accumulated coverage for one source file, keyed by line number.
A single source file can be represented in several .gcda files -- a header
compiled into every test binary, or a .c file linked more than once -- so
counts are summed across all of them rather than overwritten.
"""
def __init__(self, path):
self.path = path # repo-relative
self.lines = {} # line number -> execution count
self.branches = {} # line number -> list of branch counts
self.functions = {} # function name -> execution count
def add_line(self, lineno, count):
self.lines[lineno] = self.lines.get(lineno, 0) + count
def add_branches(self, lineno, counts):
cur = self.branches.setdefault(lineno, [0] * len(counts))
# Defensive: different translation units should agree on the branch
# count for a line, but never let a mismatch raise.
if len(cur) != len(counts):
if len(counts) > len(cur):
cur.extend([0] * (len(counts) - len(cur)))
counts = counts + [0] * (len(cur) - len(counts))
for i, c in enumerate(counts):
cur[i] += c
def add_function(self, name, count):
self.functions[name] = self.functions.get(name, 0) + count
# -- derived totals ---------------------------------------------------- #
@property
def lines_total(self):
return len(self.lines)
@property
def lines_covered(self):
return sum(1 for c in self.lines.values() if c > 0)
@property
def branches_total(self):
return sum(len(b) for b in self.branches.values())
@property
def branches_covered(self):
return sum(1 for b in self.branches.values() for c in b if c > 0)
@property
def functions_total(self):
return len(self.functions)
@property
def functions_covered(self):
return sum(1 for c in self.functions.values() if c > 0)
def uncovered_lines(self):
return sorted(n for n, c in self.lines.items() if c == 0)
def uncovered_functions(self):
return sorted(n for n, c in self.functions.items() if c == 0)
def pct(covered, total):
"""Coverage percentage. A file with nothing instrumented counts as 100%,
matching gcov/lcov: there is nothing there to leave untested."""
return 100.0 * covered / total if total else 100.0
def find_gcda(build):
return sorted(glob.glob(os.path.join(build, "**", "*.gcda"), recursive=True))
def run_gcov(gcov, gcda):
"""Run gcov on one .gcda and return the parsed JSON, or None on failure.
gcov resolves the matching .gcno relative to the .gcda, so it is invoked
from the .gcda's directory with a bare filename.
"""
workdir = os.path.dirname(gcda) or "."
cmd = [gcov, "--json-format", "--stdout", "--branch-probabilities",
os.path.basename(gcda)]
try:
proc = subprocess.run(cmd, cwd=workdir, stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
except OSError as exc:
sys.stderr.write(f"warning: cannot run {gcov}: {exc}\n")
return None
if proc.returncode != 0:
sys.stderr.write(f"warning: gcov failed on {gcda}:\n"
f"{proc.stderr.decode(errors='replace')}")
return None
text = proc.stdout.decode(errors="replace").strip()
if not text:
return None
try:
return json.loads(text)
except ValueError as exc:
sys.stderr.write(f"warning: unparseable gcov JSON for {gcda}: {exc}\n")
return None
def rel_to_root(path, cwd, root):
"""Resolve a path from gcov's JSON to a repo-relative path, or None if it
falls outside the repo (libc headers, the toolchain, an out-of-tree dep)."""
if not os.path.isabs(path):
path = os.path.join(cwd, path)
path = os.path.realpath(path)
try:
rel = os.path.relpath(path, root)
except ValueError: # different drive (Windows)
return None
if rel.startswith(os.pardir):
return None
return rel.replace(os.sep, "/")
def selected(rel, includes, excludes):
"""True if a repo-relative path passes the include/exclude prefix filters.
Excludes win, so `--include src --exclude src/generated` behaves sanely."""
def under(prefix):
return rel == prefix or rel.startswith(prefix.rstrip("/") + "/")
if any(under(e) for e in excludes):
return False
if not includes:
return True
return any(under(i) for i in includes)
def collect(build, root, gcov, includes, excludes):
"""Aggregate coverage for every selected source file in the build tree."""
files = {}
gcda_files = find_gcda(build)
for gcda in gcda_files:
data = run_gcov(gcov, gcda)
if not data:
continue
cwd = data.get("current_working_directory") or os.path.dirname(gcda)
for entry in data.get("files", []):
rel = rel_to_root(entry.get("file", ""), cwd, root)
if rel is None or not selected(rel, includes, excludes):
continue
fc = files.setdefault(rel, FileCoverage(rel))
for line in entry.get("lines", []):
lineno = line.get("line_number")
if lineno is None:
continue
fc.add_line(lineno, line.get("count", 0))
branches = line.get("branches") or []
if branches:
fc.add_branches(lineno, [b.get("count", 0) for b in branches])
for fn in entry.get("functions", []):
name = fn.get("demangled_name") or fn.get("name")
if name:
fc.add_function(name, fn.get("execution_count", 0))
return files, len(gcda_files)
# --------------------------------------------------------------------------- #
# Reporting
# --------------------------------------------------------------------------- #
def line_ranges(numbers):
"""Compress [3,4,5,9,11,12] into ['3-5', '9', '11-12'] for readability."""
out = []
start = prev = None
for n in numbers:
if start is None:
start = prev = n
elif n == prev + 1:
prev = n
else:
out.append(str(start) if start == prev else f"{start}-{prev}")
start = prev = n
if start is not None:
out.append(str(start) if start == prev else f"{start}-{prev}")
return out
def report(files, summary_only):
"""Render the text report. Returns (totals dict, report text)."""
names = sorted(files)
width = max([len(n) for n in names] + [len("TOTAL")])
out = []
header = (f" {'file'.ljust(width)} {'lines':>15} {'branches':>15} "
f"{'functions':>15}")
out.append("=" * len(header))
out.append("CODE COVERAGE SUMMARY")
out.append("=" * len(header))
out.append(header)
out.append(" " + "-" * (len(header) - 2))
def row(name, lc, lt, bc, bt, fc_, ft):
cell = lambda c, t: f"{pct(c, t):5.1f}% {c:4d}/{t:<4d}"
out.append(f" {name.ljust(width)} {cell(lc, lt):>15} "
f"{cell(bc, bt):>15} {cell(fc_, ft):>15}")
tot = [0, 0, 0, 0, 0, 0]
for name in names:
f = files[name]
vals = (f.lines_covered, f.lines_total, f.branches_covered,
f.branches_total, f.functions_covered, f.functions_total)
row(name, *vals)
tot = [t + v for t, v in zip(tot, vals)]
out.append(" " + "-" * (len(header) - 2))
row("TOTAL", *tot)
if not summary_only:
for name in names:
f = files[name]
gaps = f.uncovered_lines()
missing_fns = f.uncovered_functions()
if not gaps and not missing_fns:
continue
out.append(f"\n{name}: uncovered (each one is a missing test)")
if missing_fns:
out.append(" functions never called:")
for fn in missing_fns:
out.append(f" {fn}")
if gaps:
out.append(f" lines ({len(gaps)}): "
+ ", ".join(line_ranges(gaps)))
text = "\n".join(out) + "\n"
sys.stdout.write(text)
return text, {
"lines_covered": tot[0], "lines_total": tot[1],
"branches_covered": tot[2], "branches_total": tot[3],
"functions_covered": tot[4], "functions_total": tot[5],
}
def write_cobertura(path, files, root, totals):
"""Emit a minimal Cobertura report. One <class> per source file, which is
what CI coverage publishers expect for C."""
esc = saxutils.quoteattr
lr = pct(totals["lines_covered"], totals["lines_total"]) / 100.0
br = pct(totals["branches_covered"], totals["branches_total"]) / 100.0
out = ['<?xml version="1.0" encoding="UTF-8"?>',
f'<coverage line-rate="{lr:.4f}" branch-rate="{br:.4f}" '
f'lines-covered="{totals["lines_covered"]}" '
f'lines-valid="{totals["lines_total"]}" '
f'branches-covered="{totals["branches_covered"]}" '
f'branches-valid="{totals["branches_total"]}" '
'complexity="0" version="0" timestamp="0">',
' <sources>', f' <source>{saxutils.escape(root)}</source>',
' </sources>', ' <packages>',
' <package name="akstdlib" '
f'line-rate="{lr:.4f}" branch-rate="{br:.4f}" complexity="0">',
' <classes>']
for name in sorted(files):
f = files[name]
flr = pct(f.lines_covered, f.lines_total) / 100.0
fbr = pct(f.branches_covered, f.branches_total) / 100.0
cls = os.path.basename(name)
out.append(f' <class name={esc(cls)} filename={esc(name)} '
f'line-rate="{flr:.4f}" branch-rate="{fbr:.4f}" '
'complexity="0">')
out.append(' <methods/>')
out.append(' <lines>')
for lineno in sorted(f.lines):
count = f.lines[lineno]
brs = f.branches.get(lineno) or []
if brs:
taken = sum(1 for c in brs if c > 0)
cond = (f' branch="true" condition-coverage='
f'{esc(f"{pct(taken, len(brs)):.0f}% ({taken}/{len(brs)})")}')
else:
cond = ' branch="false"'
out.append(f' <line number="{lineno}" '
f'hits="{count}"{cond}/>')
out.append(' </lines>')
out.append(' </class>')
out += [' </classes>', ' </package>', ' </packages>',
'</coverage>']
with open(path, "w") as fh:
fh.write("\n".join(out) + "\n")
# --------------------------------------------------------------------------- #
# Driver
# --------------------------------------------------------------------------- #
def zero_counters(build):
"""Delete accumulated .gcda. gcov counters are cumulative across runs, so
without this a report mixes in every earlier suite run (and any test binary
run by hand), which quietly overstates coverage."""
removed = 0
for gcda in find_gcda(build):
try:
os.remove(gcda)
removed += 1
except OSError as exc:
sys.stderr.write(f"warning: cannot remove {gcda}: {exc}\n")
return removed
def main():
try:
sys.stdout.reconfigure(line_buffering=True)
except (AttributeError, ValueError):
pass
here = os.path.dirname(os.path.abspath(__file__))
default_root = os.path.dirname(here)
ap = argparse.ArgumentParser(description="Code coverage for libakstdlib")
ap.add_argument("--build", default="build-coverage")
ap.add_argument("--source-root", default=default_root)
ap.add_argument("--gcov", default=os.environ.get("GCOV", "gcov"))
ap.add_argument("--include", action="append", default=None)
ap.add_argument("--exclude", action="append", default=None)
ap.add_argument("--zero", action="store_true")
ap.add_argument("--run-tests", action="store_true")
ap.add_argument("--threshold", type=float, default=0.0)
ap.add_argument("--branch-threshold", type=float, default=0.0)
ap.add_argument("--summary-only", action="store_true")
ap.add_argument("--output", default=None,
help="also write the text report to this path")
ap.add_argument("--cobertura", default=None)
args = ap.parse_args()
root = os.path.realpath(args.source_root)
build = os.path.abspath(args.build)
includes = args.include if args.include is not None else DEFAULT_INCLUDE
if args.exclude is not None:
excludes = args.exclude
else:
# Excludes win over includes, so a *default* exclude must not silently
# cancel an explicit --include: `--include tests` has to report tests/
# even though tests/ is excluded by default.
excludes = [e for e in DEFAULT_EXCLUDE
if not any(selected(e, [i], []) for i in (args.include or []))]
if not os.path.isdir(build):
sys.stderr.write(f"error: no such build directory: {build}\n"
"Configure one with:\n"
" cmake -S . -B build-coverage -DAKSL_COVERAGE=ON\n"
" cmake --build build-coverage\n")
return 2
if args.zero and not args.run_tests:
n = zero_counters(build)
print(f"Cleared {n} .gcda counter file(s) in {build}")
return 0
if args.run_tests:
n = zero_counters(build)
print(f"Cleared {n} .gcda counter file(s) in {build}")
print("Running ctest ...")
rc = subprocess.call(["ctest", "--test-dir", build,
"--output-on-failure"])
if rc != 0:
# Report anyway: partial coverage from a red suite is still useful,
# and the ctest output above already shows what failed.
sys.stderr.write(f"warning: ctest exited {rc}; "
"reporting coverage from a failing suite\n")
print()
files, n_gcda = collect(build, root, args.gcov, includes, excludes)
if not n_gcda:
sys.stderr.write(
f"error: no .gcda files under {build}.\n"
"Either the build was not configured with -DAKSL_COVERAGE=ON, or\n"
"the test suite has not been run yet (try --run-tests).\n")
return 2
if not files:
sys.stderr.write(
f"error: {n_gcda} .gcda file(s) found, but none of the covered "
"sources matched\nthe include/exclude filters "
f"(include={includes}, exclude={excludes}).\n")
return 2
text, totals = report(files, args.summary_only)
# CTest swallows the output of a passing test, so also drop the report on
# disk: that is what makes the in-suite coverage_report entry useful without
# having to re-run ctest under -V.
if args.output:
path = os.path.abspath(args.output)
try:
with open(path, "w") as fh:
fh.write(text)
print(f"\nReport written to: {path}")
except OSError as exc:
sys.stderr.write(f"warning: cannot write {path}: {exc}\n")
if args.cobertura:
path = os.path.abspath(args.cobertura)
write_cobertura(path, files, root, totals)
print(f"\nCobertura report written to: {path}")
line_pct = pct(totals["lines_covered"], totals["lines_total"])
branch_pct = pct(totals["branches_covered"], totals["branches_total"])
failed = False
if args.threshold > 0 and line_pct < args.threshold:
print(f"\nFAIL: line coverage {line_pct:.1f}% < "
f"threshold {args.threshold:.1f}%")
failed = True
if args.branch_threshold > 0 and branch_pct < args.branch_threshold:
print(f"\nFAIL: branch coverage {branch_pct:.1f}% < "
f"threshold {args.branch_threshold:.1f}%")
failed = True
return 1 if failed else 0
if __name__ == "__main__":
sys.exit(main())

View File

@@ -277,8 +277,11 @@ def write_junit(path, records, targets):
def copy_tree(src, dst):
ignore = shutil.ignore_patterns("build", ".git", "*.o", "*.so", "*~",
"#*#", "*.iso", "*.png")
# "build*" covers build/, build-asan/ and build-coverage/: the harness
# configures its own tree inside the copy, so copying those is pure IO --
# and a coverage tree drags along every .gcno/.gcda as well.
ignore = shutil.ignore_patterns("build*", ".git", "*.o", "*.so", "*~",
"#*#", "*.iso", "*.png", "*.gcno", "*.gcda")
shutil.copytree(src, dst, ignore=ignore, symlinks=True)

25
scripts/reindent.el Normal file
View File

@@ -0,0 +1,25 @@
;; Canonical reindent: Emacs cc-mode "stroustrup", c-basic-offset 4,
;; indent-tabs-mode on, tab-width 8. A correct file is a fixed point of this.
;;
;; inextern-lang is set to 0 because akstdlib.h wraps its whole body in
;; extern "C" { }, and cc-mode otherwise indents every declaration in the file
;; one level for it. That is a real cc-mode behaviour rather than a bug, but it
;; is not the house style and not what the file looked like before.
;;
;; AGENTS.md forbids introducing clang-format, and this is why the tool that
;; decides what "correct" means has to be the same engine the author's editor
;; runs: this offset is not something clang-format could be made to agree with.
(require 'cc-mode)
(setq-default indent-tabs-mode t)
(setq-default tab-width 8)
(setq c-default-style "stroustrup")
(dolist (file command-line-args-left)
(find-file file)
(c-mode)
(setq indent-tabs-mode t
tab-width 8
c-basic-offset 4)
(c-set-offset 'inextern-lang 0)
(indent-region (point-min) (point-max))
(when (buffer-modified-p) (save-buffer))
(kill-buffer))

25
src/aksl_internal.h Normal file
View File

@@ -0,0 +1,25 @@
#ifndef _AKSL_INTERNAL_H_
#define _AKSL_INTERNAL_H_
/*
* Shared internals. Not installed, not part of the public API -- anything here
* is free to change without so much as a patch bump.
*/
#include <errno.h>
/*
* errno as an akerror status, with a fallback for when it is not one.
*
* A libc call is allowed to fail without touching errno -- malloc(0) may return
* NULL and leave it alone -- and errno may equally be a leftover from some
* earlier, unrelated, *successful* call. Reporting it raw produces a FAIL whose
* status is 0, which every downstream DETECT and CATCH reads as success while
* the context still holds a pool slot: an error that is invisible and leaks at
* the same time. Every errno-sourced status in this library goes through here,
* and every wrapped call clears errno first so the value read back is its own.
* TODO.md 2.2.1.
*/
#define AKSL_ERRNO_OR(__fallback) (errno != 0 ? errno : (__fallback))
#endif // _AKSL_INTERNAL_H_

1130
src/collections.c Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

570
src/stream.c Normal file
View File

@@ -0,0 +1,570 @@
/*
* stdio.h wrappers beyond fopen/fread/fwrite/fclose -- TODO.md section 3.1.
*
* Positioning, flushing, character and line I/O, stream state, formatted input,
* and the file-level operations that go with them.
*
* The recurring theme, and the reason most of these are worth wrapping at all,
* is that stdio reports failure through a return value that is easy to mistake
* for data. ftell(3) returns -1L, fgetc(3) returns EOF, fgets(3) returns NULL,
* and sscanf(3) returns a count that a caller has to compare against the number
* of conversions it wrote out by hand. Every one of those is a silent failure
* waiting for someone to forget the check once.
*
* Where a function has a genuine "nothing more to read" outcome -- fgets at the
* end of a file, getline at EOF -- that is AKERR_EOF rather than AKERR_IO, so a
* read loop can tell the end of its input from the failure of its input.
*/
#include <akstdlib.h>
#include <errno.h>
#include <limits.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include "aksl_internal.h"
/* ---------------------------------------------------------------------- */
/* Positioning */
/* ---------------------------------------------------------------------- */
akerr_ErrorContext AKERR_NOIGNORE *aksl_fseek(FILE *stream, long offset, int whence)
{
PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, stream, AKERR_NULLPOINTER, "stream=%p", (void *)stream);
errno = 0;
FAIL_NONZERO_RETURN(e, fseek(stream, offset, whence), AKSL_ERRNO_OR(AKERR_IO),
"fseek to offset %ld whence %d", offset, whence);
SUCCEED_RETURN(e);
}
/*
* ftell(3) reports failure as -1L, which is also a perfectly ordinary thing for
* an arithmetic type to hold. Out through *dest, with the failure as a status.
*/
akerr_ErrorContext AKERR_NOIGNORE *aksl_ftell(FILE *stream, long *dest)
{
long pos = 0;
PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, stream, AKERR_NULLPOINTER, "stream=%p, dest=%p", (void *)stream, (void *)dest);
FAIL_ZERO_RETURN(e, dest, AKERR_NULLPOINTER, "stream=%p, dest=%p", (void *)stream, (void *)dest);
*dest = 0;
errno = 0;
pos = ftell(stream);
FAIL_NONZERO_RETURN(e, (pos == -1L), AKSL_ERRNO_OR(AKERR_IO), "ftell failed");
*dest = pos;
SUCCEED_RETURN(e);
}
/*
* rewind(3) is the one positioning call with no error return at all: it is
* fseek(stream, 0, SEEK_SET) with the result thrown away, and it clears the
* error indicator on the way past so even that evidence is gone. This is the
* fseek, so a rewind that cannot happen is reported rather than assumed.
*/
akerr_ErrorContext AKERR_NOIGNORE *aksl_rewind(FILE *stream)
{
PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, stream, AKERR_NULLPOINTER, "stream=%p", (void *)stream);
errno = 0;
FAIL_NONZERO_RETURN(e, fseek(stream, 0L, SEEK_SET), AKSL_ERRNO_OR(AKERR_IO),
"rewind failed");
clearerr(stream);
SUCCEED_RETURN(e);
}
akerr_ErrorContext AKERR_NOIGNORE *aksl_fseeko(FILE *stream, off_t offset, int whence)
{
PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, stream, AKERR_NULLPOINTER, "stream=%p", (void *)stream);
errno = 0;
FAIL_NONZERO_RETURN(e, fseeko(stream, offset, whence), AKSL_ERRNO_OR(AKERR_IO),
"fseeko to offset %lld whence %d", (long long)offset, whence);
SUCCEED_RETURN(e);
}
akerr_ErrorContext AKERR_NOIGNORE *aksl_ftello(FILE *stream, off_t *dest)
{
off_t pos = 0;
PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, stream, AKERR_NULLPOINTER, "stream=%p, dest=%p", (void *)stream, (void *)dest);
FAIL_ZERO_RETURN(e, dest, AKERR_NULLPOINTER, "stream=%p, dest=%p", (void *)stream, (void *)dest);
*dest = 0;
errno = 0;
pos = ftello(stream);
FAIL_NONZERO_RETURN(e, (pos == (off_t)-1), AKSL_ERRNO_OR(AKERR_IO), "ftello failed");
*dest = pos;
SUCCEED_RETURN(e);
}
/*
* fgetpos/fsetpos carry an opaque fpos_t rather than a byte offset, which is
* what makes them the right pair for a stream in a multibyte locale where a
* byte offset is not enough to restore the conversion state.
*/
akerr_ErrorContext AKERR_NOIGNORE *aksl_fgetpos(FILE *stream, fpos_t *pos)
{
PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, stream, AKERR_NULLPOINTER, "stream=%p, pos=%p", (void *)stream, (void *)pos);
FAIL_ZERO_RETURN(e, pos, AKERR_NULLPOINTER, "stream=%p, pos=%p", (void *)stream, (void *)pos);
errno = 0;
FAIL_NONZERO_RETURN(e, fgetpos(stream, pos), AKSL_ERRNO_OR(AKERR_IO), "fgetpos failed");
SUCCEED_RETURN(e);
}
akerr_ErrorContext AKERR_NOIGNORE *aksl_fsetpos(FILE *stream, const fpos_t *pos)
{
PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, stream, AKERR_NULLPOINTER, "stream=%p, pos=%p", (void *)stream, (void *)pos);
FAIL_ZERO_RETURN(e, pos, AKERR_NULLPOINTER, "stream=%p, pos=%p", (void *)stream, (void *)pos);
errno = 0;
FAIL_NONZERO_RETURN(e, fsetpos(stream, pos), AKSL_ERRNO_OR(AKERR_IO), "fsetpos failed");
SUCCEED_RETURN(e);
}
/* ---------------------------------------------------------------------- */
/* Flushing and buffering */
/* ---------------------------------------------------------------------- */
/*
* NULL is legal here and means "every output stream", which is fflush(3)'s own
* documented behaviour and genuinely useful before a fork or an abort -- so
* unlike almost everywhere else in this library, a NULL argument is not an
* error. It is also the only way to find out that buffered data could not be
* written before the process goes away.
*/
akerr_ErrorContext AKERR_NOIGNORE *aksl_fflush(FILE *stream)
{
PREPARE_ERROR(e);
errno = 0;
FAIL_NONZERO_RETURN(e, fflush(stream), AKSL_ERRNO_OR(AKERR_IO),
"fflush failed and the buffered data is lost");
SUCCEED_RETURN(e);
}
akerr_ErrorContext AKERR_NOIGNORE *aksl_setvbuf(FILE *stream, char *buf, int mode, size_t size)
{
PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, stream, AKERR_NULLPOINTER, "stream=%p", (void *)stream);
errno = 0;
FAIL_NONZERO_RETURN(e, setvbuf(stream, buf, mode, size), AKSL_ERRNO_OR(AKERR_VALUE),
"setvbuf mode %d size %zu", mode, size);
SUCCEED_RETURN(e);
}
/* ---------------------------------------------------------------------- */
/* Character and line I/O */
/* ---------------------------------------------------------------------- */
/*
* fgetc(3) folds three outcomes into one int: a byte, the end of the file, and
* a read error, the last two both spelled EOF. Split apart here -- *dest is the
* byte, AKERR_EOF is the end, and anything else is the error -- so a read loop
* can be written without the ferror/feof dance at the bottom of it.
*/
akerr_ErrorContext AKERR_NOIGNORE *aksl_fgetc(FILE *stream, int *dest)
{
int c = 0;
PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, stream, AKERR_NULLPOINTER, "stream=%p, dest=%p", (void *)stream, (void *)dest);
FAIL_ZERO_RETURN(e, dest, AKERR_NULLPOINTER, "stream=%p, dest=%p", (void *)stream, (void *)dest);
*dest = 0;
errno = 0;
c = fgetc(stream);
if ( c == EOF ) {
FAIL_NONZERO_RETURN(e, ferror(stream), AKSL_ERRNO_OR(AKERR_IO), "fgetc failed");
FAIL_RETURN(e, AKERR_EOF, "end of stream");
}
*dest = c;
SUCCEED_RETURN(e);
}
akerr_ErrorContext AKERR_NOIGNORE *aksl_fputc(int c, FILE *stream)
{
PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, stream, AKERR_NULLPOINTER, "stream=%p", (void *)stream);
errno = 0;
FAIL_NONZERO_RETURN(e, (fputc(c, stream) == EOF), AKSL_ERRNO_OR(AKERR_IO),
"fputc of 0x%02x failed", (unsigned)c & 0xffu);
SUCCEED_RETURN(e);
}
/* Pushes one byte back so the next read returns it. One byte is all that is
* guaranteed; a second ungetc without a read in between may well fail. */
akerr_ErrorContext AKERR_NOIGNORE *aksl_ungetc(int c, FILE *stream)
{
PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, stream, AKERR_NULLPOINTER, "stream=%p", (void *)stream);
errno = 0;
FAIL_NONZERO_RETURN(e, (ungetc(c, stream) == EOF), AKSL_ERRNO_OR(AKERR_IO),
"ungetc of 0x%02x failed", (unsigned)c & 0xffu);
SUCCEED_RETURN(e);
}
/*
* fgets(3) into a bounded buffer, with the length read reported and the three
* outcomes separated. A line too long for the buffer is *not* an error -- it is
* a short read that leaves the rest of the line in the stream, which is how
* fgets works and how a caller reading fixed-size chunks wants it -- but
* *len_out lets the caller notice, because a full buffer with no trailing
* newline is exactly that case.
*/
akerr_ErrorContext AKERR_NOIGNORE *aksl_fgets(char *s, size_t size, FILE *stream, size_t *len_out)
{
char *result = NULL;
PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, len_out, AKERR_NULLPOINTER, "s=%p, stream=%p, len_out=%p",
(void *)s, (void *)stream, (void *)len_out);
*len_out = 0;
FAIL_ZERO_RETURN(e, s, AKERR_NULLPOINTER, "s=%p, stream=%p, len_out=%p",
(void *)s, (void *)stream, (void *)len_out);
FAIL_ZERO_RETURN(e, stream, AKERR_NULLPOINTER, "s=%p, stream=%p, len_out=%p",
(void *)s, (void *)stream, (void *)len_out);
/* fgets takes an int, and a size that does not fit one is a caller error. */
FAIL_NONZERO_RETURN(e, (size == 0 || size > (size_t)INT_MAX), AKERR_VALUE,
"size %zu is outside the range fgets accepts", size);
s[0] = '\0';
errno = 0;
result = fgets(s, (int)size, stream);
if ( result == NULL ) {
FAIL_NONZERO_RETURN(e, ferror(stream), AKSL_ERRNO_OR(AKERR_IO), "fgets failed");
FAIL_RETURN(e, AKERR_EOF, "end of stream");
}
*len_out = strlen(s);
SUCCEED_RETURN(e);
}
akerr_ErrorContext AKERR_NOIGNORE *aksl_fputs(const char *s, FILE *stream)
{
PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, s, AKERR_NULLPOINTER, "s=%p, stream=%p", (void *)s, (void *)stream);
FAIL_ZERO_RETURN(e, stream, AKERR_NULLPOINTER, "s=%p, stream=%p", (void *)s, (void *)stream);
errno = 0;
FAIL_NONZERO_RETURN(e, (fputs(s, stream) == EOF), AKSL_ERRNO_OR(AKERR_IO), "fputs failed");
SUCCEED_RETURN(e);
}
/*
* getline(3) grows *lineptr as needed, so the caller starts with
* `char *line = NULL; size_t cap = 0;` and releases the buffer with aksl_free
* once the loop is done -- not once per line. *len_out is the length read,
* which is what tells an embedded NUL from the end of the line; strlen cannot.
*/
akerr_ErrorContext AKERR_NOIGNORE *aksl_getline(char **lineptr, size_t *n, FILE *stream, size_t *len_out)
{
ssize_t got = 0;
PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, len_out, AKERR_NULLPOINTER, "lineptr=%p, n=%p, stream=%p, len_out=%p",
(void *)lineptr, (void *)n, (void *)stream, (void *)len_out);
*len_out = 0;
FAIL_ZERO_RETURN(e, lineptr, AKERR_NULLPOINTER, "lineptr=%p, n=%p, stream=%p, len_out=%p",
(void *)lineptr, (void *)n, (void *)stream, (void *)len_out);
FAIL_ZERO_RETURN(e, n, AKERR_NULLPOINTER, "lineptr=%p, n=%p, stream=%p, len_out=%p",
(void *)lineptr, (void *)n, (void *)stream, (void *)len_out);
FAIL_ZERO_RETURN(e, stream, AKERR_NULLPOINTER, "lineptr=%p, n=%p, stream=%p, len_out=%p",
(void *)lineptr, (void *)n, (void *)stream, (void *)len_out);
errno = 0;
got = getline(lineptr, n, stream);
if ( got < 0 ) {
FAIL_NONZERO_RETURN(e, ferror(stream), AKSL_ERRNO_OR(AKERR_IO), "getline failed");
FAIL_RETURN(e, AKERR_EOF, "end of stream");
}
*len_out = (size_t)got;
SUCCEED_RETURN(e);
}
akerr_ErrorContext AKERR_NOIGNORE *aksl_getdelim(char **lineptr, size_t *n, int delim,
FILE *stream, size_t *len_out)
{
ssize_t got = 0;
PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, len_out, AKERR_NULLPOINTER, "lineptr=%p, n=%p, stream=%p, len_out=%p",
(void *)lineptr, (void *)n, (void *)stream, (void *)len_out);
*len_out = 0;
FAIL_ZERO_RETURN(e, lineptr, AKERR_NULLPOINTER, "lineptr=%p, n=%p, stream=%p, len_out=%p",
(void *)lineptr, (void *)n, (void *)stream, (void *)len_out);
FAIL_ZERO_RETURN(e, n, AKERR_NULLPOINTER, "lineptr=%p, n=%p, stream=%p, len_out=%p",
(void *)lineptr, (void *)n, (void *)stream, (void *)len_out);
FAIL_ZERO_RETURN(e, stream, AKERR_NULLPOINTER, "lineptr=%p, n=%p, stream=%p, len_out=%p",
(void *)lineptr, (void *)n, (void *)stream, (void *)len_out);
errno = 0;
got = getdelim(lineptr, n, delim, stream);
if ( got < 0 ) {
FAIL_NONZERO_RETURN(e, ferror(stream), AKSL_ERRNO_OR(AKERR_IO), "getdelim failed");
FAIL_RETURN(e, AKERR_EOF, "end of stream");
}
*len_out = (size_t)got;
SUCCEED_RETURN(e);
}
/* ---------------------------------------------------------------------- */
/* Stream state */
/* ---------------------------------------------------------------------- */
/*
* feof and ferror answer through an out-param, exposed so that a caller never
* has to reach into a FILE * itself. They are thin, and that is the point: the
* NULL check is the whole value.
*/
akerr_ErrorContext AKERR_NOIGNORE *aksl_feof(FILE *stream, int *dest)
{
PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, stream, AKERR_NULLPOINTER, "stream=%p, dest=%p", (void *)stream, (void *)dest);
FAIL_ZERO_RETURN(e, dest, AKERR_NULLPOINTER, "stream=%p, dest=%p", (void *)stream, (void *)dest);
*dest = feof(stream);
SUCCEED_RETURN(e);
}
akerr_ErrorContext AKERR_NOIGNORE *aksl_ferror(FILE *stream, int *dest)
{
PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, stream, AKERR_NULLPOINTER, "stream=%p, dest=%p", (void *)stream, (void *)dest);
FAIL_ZERO_RETURN(e, dest, AKERR_NULLPOINTER, "stream=%p, dest=%p", (void *)stream, (void *)dest);
*dest = ferror(stream);
SUCCEED_RETURN(e);
}
akerr_ErrorContext AKERR_NOIGNORE *aksl_clearerr(FILE *stream)
{
PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, stream, AKERR_NULLPOINTER, "stream=%p", (void *)stream);
clearerr(stream);
SUCCEED_RETURN(e);
}
akerr_ErrorContext AKERR_NOIGNORE *aksl_fileno(FILE *stream, int *dest)
{
int fd = -1;
PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, stream, AKERR_NULLPOINTER, "stream=%p, dest=%p", (void *)stream, (void *)dest);
FAIL_ZERO_RETURN(e, dest, AKERR_NULLPOINTER, "stream=%p, dest=%p", (void *)stream, (void *)dest);
*dest = -1;
errno = 0;
fd = fileno(stream);
FAIL_NONZERO_RETURN(e, (fd < 0), AKSL_ERRNO_OR(EBADF), "stream has no descriptor");
*dest = fd;
SUCCEED_RETURN(e);
}
/* ---------------------------------------------------------------------- */
/* Opening by other means */
/* ---------------------------------------------------------------------- */
akerr_ErrorContext AKERR_NOIGNORE *aksl_freopen(const char *pathname, const char *mode,
FILE *stream, FILE **fp)
{
PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, fp, AKERR_NULLPOINTER, "pathname=%p, mode=%p, stream=%p, fp=%p",
(void *)pathname, (void *)mode, (void *)stream, (void *)fp);
*fp = NULL;
FAIL_ZERO_RETURN(e, mode, AKERR_NULLPOINTER, "pathname=%p, mode=%p, stream=%p, fp=%p",
(void *)pathname, (void *)mode, (void *)stream, (void *)fp);
FAIL_ZERO_RETURN(e, stream, AKERR_NULLPOINTER, "pathname=%p, mode=%p, stream=%p, fp=%p",
(void *)pathname, (void *)mode, (void *)stream, (void *)fp);
/*
* pathname NULL is legal and means "reopen the same file with a new mode",
* which is the one thing freopen can do that fopen cannot -- so it is not
* checked, unlike aksl_fopen's.
*/
errno = 0;
*fp = freopen(pathname, mode, stream);
FAIL_ZERO_RETURN(e, *fp, AKSL_ERRNO_OR(AKERR_IO), "%s", pathname != NULL ? pathname : "(same file)");
SUCCEED_RETURN(e);
}
akerr_ErrorContext AKERR_NOIGNORE *aksl_fdopen(int fd, const char *mode, FILE **fp)
{
PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, fp, AKERR_NULLPOINTER, "mode=%p, fp=%p", (void *)mode, (void *)fp);
*fp = NULL;
FAIL_ZERO_RETURN(e, mode, AKERR_NULLPOINTER, "mode=%p, fp=%p", (void *)mode, (void *)fp);
FAIL_NONZERO_RETURN(e, (fd < 0), AKERR_VALUE, "fd=%d is not a descriptor", fd);
errno = 0;
*fp = fdopen(fd, mode);
FAIL_ZERO_RETURN(e, *fp, AKSL_ERRNO_OR(AKERR_IO), "fd=%d mode=%s", fd, mode);
SUCCEED_RETURN(e);
}
akerr_ErrorContext AKERR_NOIGNORE *aksl_tmpfile(FILE **fp)
{
PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, fp, AKERR_NULLPOINTER, "fp=%p", (void *)fp);
*fp = NULL;
errno = 0;
*fp = tmpfile();
FAIL_ZERO_RETURN(e, *fp, AKSL_ERRNO_OR(AKERR_IO), "could not create a temporary file");
SUCCEED_RETURN(e);
}
/* ---------------------------------------------------------------------- */
/* Formatted input */
/* ---------------------------------------------------------------------- */
/*
* The scanf family's return value is the number of conversions that succeeded,
* which the caller is expected to compare against the number it wrote in the
* format string -- by hand, from memory, at every call site. Getting that wrong
* leaves the unassigned arguments holding whatever they held before, which for
* the usual uninitialised local is anything at all.
*
* So `expected` is an argument: say how many conversions must succeed, and
* anything less is AKERR_VALUE with both counts in the message. Pass 0 to opt
* out and read the count from *assigned yourself.
*/
akerr_ErrorContext AKERR_NOIGNORE *aksl_vsscanf(const char *str, const char *format,
int expected, int *assigned, va_list args)
{
int got = 0;
PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, assigned, AKERR_NULLPOINTER, "str=%p, format=%p, assigned=%p",
(void *)str, (void *)format, (void *)assigned);
*assigned = 0;
FAIL_ZERO_RETURN(e, str, AKERR_NULLPOINTER, "str=%p, format=%p, assigned=%p",
(void *)str, (void *)format, (void *)assigned);
FAIL_ZERO_RETURN(e, format, AKERR_NULLPOINTER, "str=%p, format=%p, assigned=%p",
(void *)str, (void *)format, (void *)assigned);
errno = 0;
got = vsscanf(str, format, args);
FAIL_NONZERO_RETURN(e, (got == EOF), AKSL_ERRNO_OR(AKERR_VALUE),
"no conversions were possible on \"%s\"", str);
*assigned = got;
FAIL_NONZERO_RETURN(e, (expected > 0 && got < expected), AKERR_VALUE,
"%d of %d conversions succeeded on \"%s\"", got, expected, str);
SUCCEED_RETURN(e);
}
akerr_ErrorContext AKERR_NOIGNORE *aksl_sscanf(const char *str, const char *format,
int expected, int *assigned, ...)
{
va_list args;
akerr_ErrorContext *raised = NULL;
va_start(args, assigned);
raised = aksl_vsscanf(str, format, expected, assigned, args);
va_end(args);
return raised;
}
akerr_ErrorContext AKERR_NOIGNORE *aksl_vfscanf(FILE *stream, const char *format,
int expected, int *assigned, va_list args)
{
int got = 0;
PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, assigned, AKERR_NULLPOINTER, "stream=%p, format=%p, assigned=%p",
(void *)stream, (void *)format, (void *)assigned);
*assigned = 0;
FAIL_ZERO_RETURN(e, stream, AKERR_NULLPOINTER, "stream=%p, format=%p, assigned=%p",
(void *)stream, (void *)format, (void *)assigned);
FAIL_ZERO_RETURN(e, format, AKERR_NULLPOINTER, "stream=%p, format=%p, assigned=%p",
(void *)stream, (void *)format, (void *)assigned);
errno = 0;
got = vfscanf(stream, format, args);
if ( got == EOF ) {
FAIL_NONZERO_RETURN(e, ferror(stream), AKSL_ERRNO_OR(AKERR_IO), "fscanf failed");
FAIL_RETURN(e, AKERR_EOF, "end of stream before any conversion");
}
*assigned = got;
FAIL_NONZERO_RETURN(e, (expected > 0 && got < expected), AKERR_VALUE,
"%d of %d conversions succeeded", got, expected);
SUCCEED_RETURN(e);
}
akerr_ErrorContext AKERR_NOIGNORE *aksl_fscanf(FILE *stream, const char *format,
int expected, int *assigned, ...)
{
va_list args;
akerr_ErrorContext *raised = NULL;
va_start(args, assigned);
raised = aksl_vfscanf(stream, format, expected, assigned, args);
va_end(args);
return raised;
}
/* stdin, for symmetry with aksl_printf writing to stdout. */
akerr_ErrorContext AKERR_NOIGNORE *aksl_scanf(const char *format,
int expected, int *assigned, ...)
{
va_list args;
akerr_ErrorContext *raised = NULL;
va_start(args, assigned);
raised = aksl_vfscanf(stdin, format, expected, assigned, args);
va_end(args);
return raised;
}
akerr_ErrorContext AKERR_NOIGNORE *aksl_vscanf(const char *format, int expected,
int *assigned, va_list args)
{
return aksl_vfscanf(stdin, format, expected, assigned, args);
}
/* ---------------------------------------------------------------------- */
/* Files */
/* ---------------------------------------------------------------------- */
akerr_ErrorContext AKERR_NOIGNORE *aksl_remove(const char *pathname)
{
PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, pathname, AKERR_NULLPOINTER, "pathname=%p", (void *)pathname);
errno = 0;
FAIL_NONZERO_RETURN(e, remove(pathname), AKSL_ERRNO_OR(AKERR_IO), "%s", pathname);
SUCCEED_RETURN(e);
}
akerr_ErrorContext AKERR_NOIGNORE *aksl_rename(const char *oldpath, const char *newpath)
{
PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, oldpath, AKERR_NULLPOINTER, "oldpath=%p, newpath=%p",
(void *)oldpath, (void *)newpath);
FAIL_ZERO_RETURN(e, newpath, AKERR_NULLPOINTER, "oldpath=%p, newpath=%p",
(void *)oldpath, (void *)newpath);
errno = 0;
FAIL_NONZERO_RETURN(e, rename(oldpath, newpath), AKSL_ERRNO_OR(AKERR_IO),
"%s -> %s", oldpath, newpath);
SUCCEED_RETURN(e);
}
/*
* mkstemp and mkdtemp both rewrite the template in place, so the template must
* be a writable buffer ending in exactly six X characters -- a string literal
* is a segfault, and that is checked here rather than left to the kernel. The
* caller owns the resulting file or directory, including removing it.
*/
akerr_ErrorContext AKERR_NOIGNORE *aksl_mkstemp(char *template_, int *fd)
{
size_t len = 0;
PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, fd, AKERR_NULLPOINTER, "template=%p, fd=%p", (void *)template_, (void *)fd);
*fd = -1;
FAIL_ZERO_RETURN(e, template_, AKERR_NULLPOINTER, "template=%p, fd=%p",
(void *)template_, (void *)fd);
len = strlen(template_);
FAIL_NONZERO_RETURN(e, (len < 6 || strcmp(template_ + len - 6, "XXXXXX") != 0),
AKERR_VALUE,
"template \"%s\" must end in six literal X characters", template_);
errno = 0;
*fd = mkstemp(template_);
FAIL_NONZERO_RETURN(e, (*fd < 0), AKSL_ERRNO_OR(AKERR_IO), "template \"%s\"", template_);
SUCCEED_RETURN(e);
}
akerr_ErrorContext AKERR_NOIGNORE *aksl_mkdtemp(char *template_)
{
size_t len = 0;
PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, template_, AKERR_NULLPOINTER, "template=%p", (void *)template_);
len = strlen(template_);
FAIL_NONZERO_RETURN(e, (len < 6 || strcmp(template_ + len - 6, "XXXXXX") != 0),
AKERR_VALUE,
"template \"%s\" must end in six literal X characters", template_);
errno = 0;
FAIL_ZERO_RETURN(e, mkdtemp(template_), AKSL_ERRNO_OR(AKERR_IO),
"template \"%s\"", template_);
SUCCEED_RETURN(e);
}

486
src/string.c Normal file
View File

@@ -0,0 +1,486 @@
/*
* string.h wrappers -- TODO.md section 3.1.
*
* This is the section akbasic needed most and could not have: across its source
* it calls strlen 37 times, strcmp 16, strncpy 15 and strstr once, every one of
* them raw because there was nothing here to call instead. Ten of those sites
* are the same idiom written out by hand -- a length check, then strncpy, then
* an explicit NUL -- which is exactly the "truncation reported as an error
* rather than silently accepted" that TODO.md 3.1 asks for.
*
* Two conventions run through the whole file.
*
* The copying functions take the size of the destination, and the ones named
* after libc functions that do not take one take it anyway. strcpy(3) and
* strcat(3) cannot be called safely without knowing how much room the
* destination has, and a wrapper that accepts the same arguments as strcpy
* would be an error-handling wrapper around a buffer overflow. Truncation is
* AKERR_OUTOFBOUNDS: it is the failure these functions exist to report, and
* nothing is written to the destination when it happens, so a caller who
* ignores the status does not get a half-copied string either. aksl_strncpy
* also always terminates, which strncpy(3) famously does not.
*
* The searching functions answer through an out-param and treat "not found" as
* a successful answer of NULL rather than as an error -- absent is an ordinary
* answer to "where is this", and a library that raised on it would have every
* caller handling a non-error as an error.
*/
#include <akstdlib.h>
#include <ctype.h>
#include <errno.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include "aksl_internal.h"
/* ---------------------------------------------------------------------- */
/* Length */
/* ---------------------------------------------------------------------- */
akerr_ErrorContext AKERR_NOIGNORE *aksl_strlen(const char *s, size_t *dest)
{
PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, s, AKERR_NULLPOINTER, "s=%p, dest=%p", (void *)s, (void *)dest);
FAIL_ZERO_RETURN(e, dest, AKERR_NULLPOINTER, "s=%p, dest=%p", (void *)s, (void *)dest);
*dest = strlen(s);
SUCCEED_RETURN(e);
}
/*
* strnlen(3) stops at maxlen whether or not it found a terminator, so a result
* equal to maxlen means "at least this long" rather than "this long". That
* ambiguity is the reason to use it -- it is how you measure a buffer that may
* not be terminated -- so it is reported rather than hidden: *dest is the
* length and the call succeeds either way.
*/
akerr_ErrorContext AKERR_NOIGNORE *aksl_strnlen(const char *s, size_t maxlen, size_t *dest)
{
PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, s, AKERR_NULLPOINTER, "s=%p, dest=%p", (void *)s, (void *)dest);
FAIL_ZERO_RETURN(e, dest, AKERR_NULLPOINTER, "s=%p, dest=%p", (void *)s, (void *)dest);
*dest = strnlen(s, maxlen);
SUCCEED_RETURN(e);
}
/* ---------------------------------------------------------------------- */
/* Copying and concatenation */
/* ---------------------------------------------------------------------- */
/*
* Bounded copy. dstsize is the whole destination buffer, terminator included,
* so `char buf[64]` pairs with `sizeof(buf)`. A source that does not fit is
* AKERR_OUTOFBOUNDS and the destination is left as an empty string rather than
* as a truncated one -- a caller who ignores the error gets nothing, which is
* far easier to notice than a plausible-looking prefix.
*/
akerr_ErrorContext AKERR_NOIGNORE *aksl_strcpy(char *dst, size_t dstsize, const char *src)
{
size_t len = 0;
PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, dst, AKERR_NULLPOINTER, "dst=%p, src=%p", (void *)dst, (void *)src);
FAIL_ZERO_RETURN(e, src, AKERR_NULLPOINTER, "dst=%p, src=%p", (void *)dst, (void *)src);
FAIL_ZERO_RETURN(e, dstsize, AKERR_VALUE, "dstsize=0 leaves no room for a terminator");
dst[0] = '\0';
len = strlen(src);
FAIL_NONZERO_RETURN(e, (len >= dstsize), AKERR_OUTOFBOUNDS,
"source is %zu bytes and the destination holds %zu including the terminator",
len, dstsize);
memcpy(dst, src, len + 1);
SUCCEED_RETURN(e);
}
/*
* At most n bytes of src, and always terminated.
*
* strncpy(3) does two surprising things that this does not: it leaves the
* destination unterminated when the source is at least n bytes long, and it
* pads the remainder with NULs when the source is shorter, which turns a short
* copy into a full-length write. Here n bounds how much of the source is
* considered, dstsize bounds the write, and the result is always a C string.
*/
akerr_ErrorContext AKERR_NOIGNORE *aksl_strncpy(char *dst, size_t dstsize, const char *src, size_t n)
{
size_t len = 0;
PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, dst, AKERR_NULLPOINTER, "dst=%p, src=%p", (void *)dst, (void *)src);
FAIL_ZERO_RETURN(e, src, AKERR_NULLPOINTER, "dst=%p, src=%p", (void *)dst, (void *)src);
FAIL_ZERO_RETURN(e, dstsize, AKERR_VALUE, "dstsize=0 leaves no room for a terminator");
dst[0] = '\0';
len = strnlen(src, n);
FAIL_NONZERO_RETURN(e, (len >= dstsize), AKERR_OUTOFBOUNDS,
"%zu bytes to copy and the destination holds %zu including the terminator",
len, dstsize);
memcpy(dst, src, len);
dst[len] = '\0';
SUCCEED_RETURN(e);
}
/*
* Bounded append. dstsize is again the whole buffer, so the room actually
* available is dstsize minus what is already in there. The destination must
* already be a terminated string within dstsize; if it is not, that is
* AKERR_VALUE rather than a walk off the end looking for a NUL that is not
* there.
*/
akerr_ErrorContext AKERR_NOIGNORE *aksl_strcat(char *dst, size_t dstsize, const char *src)
{
size_t used = 0;
size_t len = 0;
PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, dst, AKERR_NULLPOINTER, "dst=%p, src=%p", (void *)dst, (void *)src);
FAIL_ZERO_RETURN(e, src, AKERR_NULLPOINTER, "dst=%p, src=%p", (void *)dst, (void *)src);
FAIL_ZERO_RETURN(e, dstsize, AKERR_VALUE, "dstsize=0 leaves no room for a terminator");
used = strnlen(dst, dstsize);
FAIL_NONZERO_RETURN(e, (used == dstsize), AKERR_VALUE,
"destination is not terminated within its %zu bytes", dstsize);
len = strlen(src);
FAIL_NONZERO_RETURN(e, (used + len >= dstsize), AKERR_OUTOFBOUNDS,
"%zu bytes in use plus %zu to append exceeds the %zu-byte destination",
used, len, dstsize);
memcpy(dst + used, src, len + 1);
SUCCEED_RETURN(e);
}
akerr_ErrorContext AKERR_NOIGNORE *aksl_strncat(char *dst, size_t dstsize, const char *src, size_t n)
{
size_t used = 0;
size_t len = 0;
PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, dst, AKERR_NULLPOINTER, "dst=%p, src=%p", (void *)dst, (void *)src);
FAIL_ZERO_RETURN(e, src, AKERR_NULLPOINTER, "dst=%p, src=%p", (void *)dst, (void *)src);
FAIL_ZERO_RETURN(e, dstsize, AKERR_VALUE, "dstsize=0 leaves no room for a terminator");
used = strnlen(dst, dstsize);
FAIL_NONZERO_RETURN(e, (used == dstsize), AKERR_VALUE,
"destination is not terminated within its %zu bytes", dstsize);
len = strnlen(src, n);
FAIL_NONZERO_RETURN(e, (used + len >= dstsize), AKERR_OUTOFBOUNDS,
"%zu bytes in use plus %zu to append exceeds the %zu-byte destination",
used, len, dstsize);
memcpy(dst + used, src, len);
dst[used + len] = '\0';
SUCCEED_RETURN(e);
}
/* ---------------------------------------------------------------------- */
/* Duplication */
/* ---------------------------------------------------------------------- */
/* *dest is the caller's, to release with aksl_free or aksl_freep. */
akerr_ErrorContext AKERR_NOIGNORE *aksl_strdup(const char *s, char **dest)
{
char *copy = NULL;
PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, s, AKERR_NULLPOINTER, "s=%p, dest=%p", (void *)s, (void *)dest);
FAIL_ZERO_RETURN(e, dest, AKERR_NULLPOINTER, "s=%p, dest=%p", (void *)s, (void *)dest);
*dest = NULL;
errno = 0;
copy = strdup(s);
FAIL_ZERO_RETURN(e, copy, AKSL_ERRNO_OR(ENOMEM), "%zu bytes", strlen(s) + 1);
*dest = copy;
SUCCEED_RETURN(e);
}
akerr_ErrorContext AKERR_NOIGNORE *aksl_strndup(const char *s, size_t n, char **dest)
{
char *copy = NULL;
PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, s, AKERR_NULLPOINTER, "s=%p, dest=%p", (void *)s, (void *)dest);
FAIL_ZERO_RETURN(e, dest, AKERR_NULLPOINTER, "s=%p, dest=%p", (void *)s, (void *)dest);
*dest = NULL;
errno = 0;
copy = strndup(s, n);
FAIL_ZERO_RETURN(e, copy, AKSL_ERRNO_OR(ENOMEM), "%zu bytes", n + 1);
*dest = copy;
SUCCEED_RETURN(e);
}
/* ---------------------------------------------------------------------- */
/* Comparison */
/* ---------------------------------------------------------------------- */
/*
* The comparison result goes through *dest because the return value is spoken
* for by the error context. The sign is the usual one: negative, zero or
* positive as a sorts before, equal to, or after b.
*/
akerr_ErrorContext AKERR_NOIGNORE *aksl_strcmp(const char *a, const char *b, int *dest)
{
PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, a, AKERR_NULLPOINTER, "a=%p, b=%p, dest=%p", (void *)a, (void *)b, (void *)dest);
FAIL_ZERO_RETURN(e, b, AKERR_NULLPOINTER, "a=%p, b=%p, dest=%p", (void *)a, (void *)b, (void *)dest);
FAIL_ZERO_RETURN(e, dest, AKERR_NULLPOINTER, "a=%p, b=%p, dest=%p", (void *)a, (void *)b, (void *)dest);
*dest = strcmp(a, b);
SUCCEED_RETURN(e);
}
akerr_ErrorContext AKERR_NOIGNORE *aksl_strncmp(const char *a, const char *b, size_t n, int *dest)
{
PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, a, AKERR_NULLPOINTER, "a=%p, b=%p, dest=%p", (void *)a, (void *)b, (void *)dest);
FAIL_ZERO_RETURN(e, b, AKERR_NULLPOINTER, "a=%p, b=%p, dest=%p", (void *)a, (void *)b, (void *)dest);
FAIL_ZERO_RETURN(e, dest, AKERR_NULLPOINTER, "a=%p, b=%p, dest=%p", (void *)a, (void *)b, (void *)dest);
*dest = strncmp(a, b, n);
SUCCEED_RETURN(e);
}
/*
* Case-insensitive comparison. akbasic folds case by hand at three sites --
* twice with an open-coded loop and once through <ctype.h> -- because BASIC
* verb and function names are case-insensitive while variable names are not.
* All three of those are this call.
*/
akerr_ErrorContext AKERR_NOIGNORE *aksl_strcasecmp(const char *a, const char *b, int *dest)
{
PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, a, AKERR_NULLPOINTER, "a=%p, b=%p, dest=%p", (void *)a, (void *)b, (void *)dest);
FAIL_ZERO_RETURN(e, b, AKERR_NULLPOINTER, "a=%p, b=%p, dest=%p", (void *)a, (void *)b, (void *)dest);
FAIL_ZERO_RETURN(e, dest, AKERR_NULLPOINTER, "a=%p, b=%p, dest=%p", (void *)a, (void *)b, (void *)dest);
*dest = strcasecmp(a, b);
SUCCEED_RETURN(e);
}
akerr_ErrorContext AKERR_NOIGNORE *aksl_strncasecmp(const char *a, const char *b, size_t n, int *dest)
{
PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, a, AKERR_NULLPOINTER, "a=%p, b=%p, dest=%p", (void *)a, (void *)b, (void *)dest);
FAIL_ZERO_RETURN(e, b, AKERR_NULLPOINTER, "a=%p, b=%p, dest=%p", (void *)a, (void *)b, (void *)dest);
FAIL_ZERO_RETURN(e, dest, AKERR_NULLPOINTER, "a=%p, b=%p, dest=%p", (void *)a, (void *)b, (void *)dest);
*dest = strncasecmp(a, b, n);
SUCCEED_RETURN(e);
}
/*
* Locale-aware collation. strcoll(3) has no error return of its own, so a
* malformed multibyte sequence in the current locale shows up only as errno
* being set -- which is why errno is cleared first and consulted after.
*/
akerr_ErrorContext AKERR_NOIGNORE *aksl_strcoll(const char *a, const char *b, int *dest)
{
PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, a, AKERR_NULLPOINTER, "a=%p, b=%p, dest=%p", (void *)a, (void *)b, (void *)dest);
FAIL_ZERO_RETURN(e, b, AKERR_NULLPOINTER, "a=%p, b=%p, dest=%p", (void *)a, (void *)b, (void *)dest);
FAIL_ZERO_RETURN(e, dest, AKERR_NULLPOINTER, "a=%p, b=%p, dest=%p", (void *)a, (void *)b, (void *)dest);
errno = 0;
*dest = strcoll(a, b);
FAIL_NONZERO_RETURN(e, errno, errno, "strcoll failed in the current locale");
SUCCEED_RETURN(e);
}
/* ---------------------------------------------------------------------- */
/* Searching */
/* ---------------------------------------------------------------------- */
/*
* Every function in this section writes NULL to *dest and succeeds when there
* is nothing to find. "Absent" is an answer, not a failure; raising on it would
* make every caller handle a non-error as an error, and the pool slot that
* error consumed would be pure waste.
*
* *dest points into the caller's own string, so it lives exactly as long as the
* string does and must not be freed.
*/
akerr_ErrorContext AKERR_NOIGNORE *aksl_strchr(const char *s, int c, char **dest)
{
PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, s, AKERR_NULLPOINTER, "s=%p, dest=%p", (void *)s, (void *)dest);
FAIL_ZERO_RETURN(e, dest, AKERR_NULLPOINTER, "s=%p, dest=%p", (void *)s, (void *)dest);
*dest = strchr(s, c);
SUCCEED_RETURN(e);
}
akerr_ErrorContext AKERR_NOIGNORE *aksl_strrchr(const char *s, int c, char **dest)
{
PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, s, AKERR_NULLPOINTER, "s=%p, dest=%p", (void *)s, (void *)dest);
FAIL_ZERO_RETURN(e, dest, AKERR_NULLPOINTER, "s=%p, dest=%p", (void *)s, (void *)dest);
*dest = strrchr(s, c);
SUCCEED_RETURN(e);
}
akerr_ErrorContext AKERR_NOIGNORE *aksl_strstr(const char *haystack, const char *needle, char **dest)
{
PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, haystack, AKERR_NULLPOINTER, "haystack=%p, needle=%p, dest=%p",
(void *)haystack, (void *)needle, (void *)dest);
FAIL_ZERO_RETURN(e, needle, AKERR_NULLPOINTER, "haystack=%p, needle=%p, dest=%p",
(void *)haystack, (void *)needle, (void *)dest);
FAIL_ZERO_RETURN(e, dest, AKERR_NULLPOINTER, "haystack=%p, needle=%p, dest=%p",
(void *)haystack, (void *)needle, (void *)dest);
*dest = strstr(haystack, needle);
SUCCEED_RETURN(e);
}
/*
* Case-insensitive search. strcasestr(3) is a GNU extension that also exists on
* the BSDs and musl, but it is not in any standard, so it is open-coded here
* rather than depending on _GNU_SOURCE reaching every consumer's build. The
* naive scan is the same complexity as glibc's fallback and this is not the hot
* path in anything.
*/
akerr_ErrorContext AKERR_NOIGNORE *aksl_strcasestr(const char *haystack, const char *needle, char **dest)
{
size_t hlen = 0;
size_t nlen = 0;
size_t i = 0;
PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, haystack, AKERR_NULLPOINTER, "haystack=%p, needle=%p, dest=%p",
(void *)haystack, (void *)needle, (void *)dest);
FAIL_ZERO_RETURN(e, needle, AKERR_NULLPOINTER, "haystack=%p, needle=%p, dest=%p",
(void *)haystack, (void *)needle, (void *)dest);
FAIL_ZERO_RETURN(e, dest, AKERR_NULLPOINTER, "haystack=%p, needle=%p, dest=%p",
(void *)haystack, (void *)needle, (void *)dest);
*dest = NULL;
nlen = strlen(needle);
/* An empty needle matches at the start, as strstr(3) has it. */
if ( nlen == 0 ) {
*dest = (char *)haystack;
SUCCEED_RETURN(e);
}
hlen = strlen(haystack);
if ( nlen <= hlen ) {
for ( i = 0; i <= hlen - nlen; i++ ) {
if ( strncasecmp(haystack + i, needle, nlen) == 0 ) {
*dest = (char *)(haystack + i);
break;
}
}
}
SUCCEED_RETURN(e);
}
akerr_ErrorContext AKERR_NOIGNORE *aksl_strpbrk(const char *s, const char *accept, char **dest)
{
PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, s, AKERR_NULLPOINTER, "s=%p, accept=%p, dest=%p",
(void *)s, (void *)accept, (void *)dest);
FAIL_ZERO_RETURN(e, accept, AKERR_NULLPOINTER, "s=%p, accept=%p, dest=%p",
(void *)s, (void *)accept, (void *)dest);
FAIL_ZERO_RETURN(e, dest, AKERR_NULLPOINTER, "s=%p, accept=%p, dest=%p",
(void *)s, (void *)accept, (void *)dest);
*dest = strpbrk(s, accept);
SUCCEED_RETURN(e);
}
akerr_ErrorContext AKERR_NOIGNORE *aksl_strspn(const char *s, const char *accept, size_t *dest)
{
PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, s, AKERR_NULLPOINTER, "s=%p, accept=%p, dest=%p",
(void *)s, (void *)accept, (void *)dest);
FAIL_ZERO_RETURN(e, accept, AKERR_NULLPOINTER, "s=%p, accept=%p, dest=%p",
(void *)s, (void *)accept, (void *)dest);
FAIL_ZERO_RETURN(e, dest, AKERR_NULLPOINTER, "s=%p, accept=%p, dest=%p",
(void *)s, (void *)accept, (void *)dest);
*dest = strspn(s, accept);
SUCCEED_RETURN(e);
}
akerr_ErrorContext AKERR_NOIGNORE *aksl_strcspn(const char *s, const char *reject, size_t *dest)
{
PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, s, AKERR_NULLPOINTER, "s=%p, reject=%p, dest=%p",
(void *)s, (void *)reject, (void *)dest);
FAIL_ZERO_RETURN(e, reject, AKERR_NULLPOINTER, "s=%p, reject=%p, dest=%p",
(void *)s, (void *)reject, (void *)dest);
FAIL_ZERO_RETURN(e, dest, AKERR_NULLPOINTER, "s=%p, reject=%p, dest=%p",
(void *)s, (void *)reject, (void *)dest);
*dest = strcspn(s, reject);
SUCCEED_RETURN(e);
}
/* ---------------------------------------------------------------------- */
/* Tokenising */
/* ---------------------------------------------------------------------- */
/*
* The reentrant tokeniser only. strtok(3) keeps its state in a hidden static,
* which makes two interleaved tokenisations silently corrupt each other and
* makes any use from a thread a bug, so it is not wrapped here at all -- see
* the note on threads in README.md.
*
* Running out of tokens is success with *dest NULL, for the same reason a failed
* search is: it is how the loop ends, not a fault.
*/
akerr_ErrorContext AKERR_NOIGNORE *aksl_strtok_r(char *str, const char *delim, char **saveptr, char **dest)
{
PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, delim, AKERR_NULLPOINTER, "delim=%p, saveptr=%p, dest=%p",
(void *)delim, (void *)saveptr, (void *)dest);
FAIL_ZERO_RETURN(e, saveptr, AKERR_NULLPOINTER, "delim=%p, saveptr=%p, dest=%p",
(void *)delim, (void *)saveptr, (void *)dest);
FAIL_ZERO_RETURN(e, dest, AKERR_NULLPOINTER, "delim=%p, saveptr=%p, dest=%p",
(void *)delim, (void *)saveptr, (void *)dest);
*dest = strtok_r(str, delim, saveptr);
SUCCEED_RETURN(e);
}
/*
* strsep(3) differs from strtok_r in returning empty tokens between adjacent
* delimiters, which is what you want for parsing "a::b" as three fields rather
* than two. It advances *stringp itself and sets it to NULL when done.
*/
akerr_ErrorContext AKERR_NOIGNORE *aksl_strsep(char **stringp, const char *delim, char **dest)
{
PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, stringp, AKERR_NULLPOINTER, "stringp=%p, delim=%p, dest=%p",
(void *)stringp, (void *)delim, (void *)dest);
FAIL_ZERO_RETURN(e, delim, AKERR_NULLPOINTER, "stringp=%p, delim=%p, dest=%p",
(void *)stringp, (void *)delim, (void *)dest);
FAIL_ZERO_RETURN(e, dest, AKERR_NULLPOINTER, "stringp=%p, delim=%p, dest=%p",
(void *)stringp, (void *)delim, (void *)dest);
*dest = strsep(stringp, delim);
SUCCEED_RETURN(e);
}
/* ---------------------------------------------------------------------- */
/* errno messages */
/* ---------------------------------------------------------------------- */
/*
* The message for a status, into the caller's own buffer.
*
* Deliberately not built on strerror_r(3). There are two incompatible functions
* by that name -- the XSI one returns int, the GNU one returns char * and may
* not touch the buffer at all -- and which one a translation unit gets depends
* on feature-test macros that a consumer of this library cannot influence from
* in here. strerror(3) itself is not thread-safe, so that is no way out either.
*
* libakerror already carries the full errno name table (its generrno.sh builds
* one at configure time), and its registry also knows the names of this
* library's own non-errno statuses -- AKERR_NULLPOINTER, AKERR_ITERATOR_BREAK
* and the rest -- which strerror_r could never name. So the lookup goes there,
* and a status neither table recognises comes back as its own number rather
* than as the bare string "Unknown Error", which tells the reader nothing.
*
* The message is truncated to nothing rather than to a prefix if it does not
* fit, on the same principle as aksl_strcpy above.
*/
akerr_ErrorContext AKERR_NOIGNORE *aksl_strerror(int status, char *buf, size_t buflen)
{
const char *name = NULL;
int needed = 0;
PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, buf, AKERR_NULLPOINTER, "buf=%p", (void *)buf);
FAIL_ZERO_RETURN(e, buflen, AKERR_VALUE, "buflen=0 leaves no room for a terminator");
buf[0] = '\0';
name = akerr_name_for_status(status, NULL);
/*
* akerr_name_for_status never returns NULL; it returns this exact string
* for anything it does not have a name for.
*/
if ( name == NULL || strcmp(name, "Unknown Error") == 0 ) {
needed = snprintf(buf, buflen, "Unknown status %d", status);
} else {
needed = snprintf(buf, buflen, "%s", name);
}
FAIL_NONZERO_RETURN(e, (needed < 0), AKERR_IO, "could not format status %d", status);
if ( (size_t)needed >= buflen ) {
buf[0] = '\0';
FAIL_RETURN(e, AKERR_OUTOFBOUNDS,
"the message for status %d needs %d bytes and the buffer holds %zu",
status, needed + 1, buflen);
}
SUCCEED_RETURN(e);
}

4
test.sh Normal file
View File

@@ -0,0 +1,4 @@
cmake -S . -B build
cmake --build build
ctest --test-dir build --output-on-failure --output-junit "$(pwd)/ctest-junit.xml"
python3 scripts/mutation_test.py --target src/error.c --junit mutation-junit.xml --threshold 65

View File

@@ -18,7 +18,9 @@
* AKSL_CHECK_STATUS() run an akerror-returning expression, assert on the
* status it came back with, and release the context
* so the error pool does not leak.
* AKSL_CHECK_OK() the status == 0 (success) case of the above.
* AKSL_CHECK_STATUS_MSG_CONTAINS()
* assert status first, then message content.
* AKSL_CHECK_OK() assert a call returned no error context at all.
* aksl_last_status/... the status, message and function name of the most
* recent context taken by AKSL_CHECK_STATUS.
* aksl_capture_install() swap in a capturing akerr_log_method so a test can
@@ -26,6 +28,8 @@
* unhandled-error output.
* aksl_slots_in_use() how many slots are currently checked out of
* AKERR_ARRAY_ERROR, for pool-leak assertions.
* aksl_temp_file() create an empty temp file for the stream, formatted
* output and path tests to work on.
* AKSL_RUN() run one test function and tally the result.
*
* Tests are written as a set of `static int test_xxx(void)` functions that
@@ -35,7 +39,9 @@
#include <akstdlib.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
/* ---------------------------------------------------------------------- */
/* Log capture */
@@ -93,6 +99,68 @@ static int __attribute__((unused)) aksl_slots_in_use(void)
return n;
}
/* ---------------------------------------------------------------------- */
/* Temp files */
/* ---------------------------------------------------------------------- */
#define AKSL_TMP_MAX 256
#define AKSL_TMP_TRACKED 32
static char aksl_tmp_paths[AKSL_TMP_TRACKED][AKSL_TMP_MAX];
static int aksl_tmp_count = 0;
/*
* Unlink every path aksl_temp_file() handed out. Registered with atexit, so the
* files go away even when a test returns early on a failed assertion -- which is
* the normal case for a known-failing test and for every mutant the mutation
* harness builds. Paths a test already unlinked simply fail here, harmlessly.
*/
static void aksl_temp_cleanup(void)
{
int i = 0;
for ( i = 0; i < aksl_tmp_count; i++ ) {
unlink(aksl_tmp_paths[i]);
}
aksl_tmp_count = 0;
}
/*
* Create an empty temp file under $TMPDIR (or /tmp) and write its path into buf.
* Returns 0 on success, non-zero on failure.
*
* mkstemp both names and creates the file, so a test that opens the path for
* reading is never racing another process for the name. Tests should still
* unlink what they create -- asserting on it catches a wrapper that removed or
* renamed the file -- but aksl_temp_cleanup() is the backstop.
*/
static int __attribute__((unused)) aksl_temp_file(char *buf, size_t n)
{
const char *dir = getenv("TMPDIR");
int fd = -1;
if ( dir == NULL || dir[0] == '\0' ) {
dir = "/tmp";
}
if ( (size_t)snprintf(buf, n, "%s/aksl_test_XXXXXX", dir) >= n ) {
return 1;
}
fd = mkstemp(buf);
if ( fd < 0 ) {
return 1;
}
close(fd);
if ( aksl_tmp_count < AKSL_TMP_TRACKED ) {
if ( aksl_tmp_count == 0 && atexit(&aksl_temp_cleanup) != 0 ) {
return 0; /* tracking is best-effort; the file itself is fine */
}
snprintf(aksl_tmp_paths[aksl_tmp_count], AKSL_TMP_MAX, "%s", buf);
aksl_tmp_count++;
}
return 0;
}
/* ---------------------------------------------------------------------- */
/* Taking ownership of a returned error context */
/* ---------------------------------------------------------------------- */
@@ -102,12 +170,22 @@ static char aksl_last_message[AKERR_MAX_ERROR_CONTEXT_STRING_LENGTH];
/* Sized to match akerr_ErrorContext.function, which akerror declares with
* AKERR_MAX_ERROR_FNAME_LENGTH rather than AKERR_MAX_ERROR_FUNCTION_LENGTH. */
static char aksl_last_function[AKERR_MAX_ERROR_FNAME_LENGTH];
/*
* The source location the error was raised from. Captured so that
* tests/test_pool.c can assert an error came from the wrapper it names rather
* than from somewhere further down -- an error that reports the wrong origin is
* worse than useless when the only debugging you get is a log file after the
* fact, which is the stated reason this library exists.
*/
static char aksl_last_file[AKERR_MAX_ERROR_FNAME_LENGTH];
static int aksl_last_line = 0;
/*
* Record the status/message/function of a returned context, release it back to
* the pool, and hand back the status. A NULL context means success, which is
* status 0. Every akstdlib call in a test should go through this (or through
* AKSL_CHECK_STATUS, which wraps it) so that no test leaks a pool slot.
* status 0. Failure-path assertions should go through this so that no test
* leaks a pool slot. Success-path assertions use AKSL_CHECK_OK, which also
* verifies that no bogus status-0 context was returned.
*/
static int __attribute__((unused)) aksl_take(akerr_ErrorContext *e)
{
@@ -115,6 +193,8 @@ static int __attribute__((unused)) aksl_take(akerr_ErrorContext *e)
aksl_last_message[0] = '\0';
aksl_last_function[0] = '\0';
aksl_last_file[0] = '\0';
aksl_last_line = 0;
if ( e == NULL ) {
aksl_last_status = 0;
return 0;
@@ -122,6 +202,8 @@ static int __attribute__((unused)) aksl_take(akerr_ErrorContext *e)
aksl_last_status = e->status;
snprintf(aksl_last_message, sizeof(aksl_last_message), "%s", e->message);
snprintf(aksl_last_function, sizeof(aksl_last_function), "%s", e->function);
snprintf(aksl_last_file, sizeof(aksl_last_file), "%s", e->fname);
aksl_last_line = e->lineno;
/*
* akerr_release_error is marked warn_unused_result, and a (void) cast does
* not silence that in GCC, so the result is assigned and discarded.
@@ -149,26 +231,51 @@ static int __attribute__((unused)) aksl_take(akerr_ErrorContext *e)
* The context is always released, including on the failure path, so a failing
* assertion does not also corrupt the pool-leak checks that follow it.
*/
#define AKSL_CHECK_STATUS(__expr, __expected) \
do { \
int __st = aksl_take(__expr); \
if ( __st != (__expected) ) { \
fprintf(stderr, \
" CHECK FAILED: %s\n" \
" got %d (%s) \"%s\"\n" \
" expected %d (%s)\n" \
" at %s:%d\n", \
#__expr, \
__st, akerr_name_for_status(__st, NULL), \
aksl_last_message, \
(__expected), \
akerr_name_for_status((__expected), NULL), \
__FILE__, __LINE__); \
return 1; \
} \
#define AKSL_CHECK_STATUS(__expr, __expected) \
do { \
int __st = aksl_take(__expr); \
if ( __st != (__expected) ) { \
fprintf(stderr, \
" CHECK FAILED: %s\n" \
" got %d (%s) \"%s\"\n" \
" expected %d (%s)\n" \
" at %s:%d\n", \
#__expr, \
__st, akerr_name_for_status(__st, NULL), \
aksl_last_message, \
(__expected), \
akerr_name_for_status((__expected), NULL), \
__FILE__, __LINE__); \
return 1; \
} \
} while ( 0 )
#define AKSL_CHECK_OK(__expr) AKSL_CHECK_STATUS(__expr, 0)
/*
* Success is represented by a NULL error context, not just status 0. This catches
* wrappers that incorrectly raise an error using a stale errno value of 0.
*/
#define AKSL_CHECK_OK(__expr) \
do { \
akerr_ErrorContext *__e = (__expr); \
if ( __e != NULL ) { \
int __st = aksl_take(__e); \
fprintf(stderr, \
" CHECK FAILED: %s\n" \
" returned error context with status %d (%s) \"%s\"\n" \
" expected no error context\n" \
" at %s:%d\n", \
#__expr, \
__st, akerr_name_for_status(__st, NULL), \
aksl_last_message, \
__FILE__, __LINE__); \
return 1; \
} \
aksl_last_status = 0; \
aksl_last_message[0] = '\0'; \
aksl_last_function[0] = '\0'; \
aksl_last_file[0] = '\0'; \
aksl_last_line = 0; \
} while ( 0 )
#define AKSL_CHECK_CONTAINS(needle) \
AKSL_CHECK(strstr(aksl_capture_buf, (needle)) != NULL)
@@ -176,9 +283,42 @@ static int __attribute__((unused)) aksl_take(akerr_ErrorContext *e)
#define AKSL_CHECK_NOT_CONTAINS(needle) \
AKSL_CHECK(strstr(aksl_capture_buf, (needle)) == NULL)
/* Assert on the message of the context most recently taken. */
#define AKSL_CHECK_MSG_CONTAINS(needle) \
AKSL_CHECK(strstr(aksl_last_message, (needle)) != NULL)
/*
* Message checks are secondary: the returned akerr status is the contract. Keep
* the status and message assertions coupled so tests cannot pass on text alone.
*/
#define AKSL_CHECK_STATUS_MSG_CONTAINS(__expr, __expected, __needle) \
do { \
int __st = aksl_take(__expr); \
if ( __st != (__expected) ) { \
fprintf(stderr, \
" CHECK FAILED: %s\n" \
" got %d (%s) \"%s\"\n" \
" expected %d (%s)\n" \
" at %s:%d\n", \
#__expr, \
__st, akerr_name_for_status(__st, NULL), \
aksl_last_message, \
(__expected), \
akerr_name_for_status((__expected), NULL), \
__FILE__, __LINE__); \
return 1; \
} \
if ( strstr(aksl_last_message, (__needle)) == NULL ) { \
fprintf(stderr, \
" CHECK FAILED: message from %s\n" \
" got \"%s\"\n" \
" expected substring \"%s\"\n" \
" status %d (%s)\n" \
" at %s:%d\n", \
#__expr, \
aksl_last_message, \
(__needle), \
__st, akerr_name_for_status(__st, NULL), \
__FILE__, __LINE__); \
return 1; \
} \
} while ( 0 )
/* ---------------------------------------------------------------------- */
/* Test driver */
@@ -189,22 +329,22 @@ static int __attribute__((unused)) aksl_take(akerr_ErrorContext *e)
* test left the error pool as it found it -- a wrapper that fails to release a
* context is a bug in the library, not just in the test.
*/
#define AKSL_RUN(__failures, __fn) \
do { \
int __before = aksl_slots_in_use(); \
int __r = __fn(); \
int __after = aksl_slots_in_use(); \
if ( __r != 0 ) { \
fprintf(stderr, "FAIL %s\n", #__fn); \
(__failures)++; \
} else if ( __after != __before ) { \
fprintf(stderr, \
"FAIL %s (leaked %d error pool slot(s))\n", \
#__fn, __after - __before); \
(__failures)++; \
} else { \
fprintf(stderr, "ok %s\n", #__fn); \
} \
#define AKSL_RUN(__failures, __fn) \
do { \
int __before = aksl_slots_in_use(); \
int __r = __fn(); \
int __after = aksl_slots_in_use(); \
if ( __r != 0 ) { \
fprintf(stderr, "FAIL %s\n", #__fn); \
(__failures)++; \
} else if ( __after != __before ) { \
fprintf(stderr, \
"FAIL %s (leaked %d error pool slot(s))\n", \
#__fn, __after - __before); \
(__failures)++; \
} else { \
fprintf(stderr, "ok %s\n", #__fn); \
} \
} while ( 0 )
#define AKSL_REPORT(__failures) \

View File

@@ -0,0 +1,21 @@
# A consumer of the *installed* package, not of the build tree.
#
# The test suite links the build tree, so it says nothing about whether an
# installed libakstdlib is usable -- and the two have come apart before. This is
# the smallest thing that exercises the paths only an install has: the version
# file (find_package with a version request), akstdlibConfig.cmake's
# find_dependency(akerror), and the exported akstdlib::akstdlib target.
#
# Configured standalone against CMAKE_PREFIX_PATH rather than being part of the
# main build, because being part of the main build is exactly what would let it
# pass without testing anything.
cmake_minimum_required(VERSION 3.10)
project(akstdlib_consumer LANGUAGES C)
# A version request, so akstdlibConfigVersion.cmake has to be present and has to
# agree. Deliberately MAJOR.MINOR rather than the full version: that is the
# compatibility promise the soname makes, so it is the one worth asserting.
find_package(akstdlib 0.2 REQUIRED)
add_executable(consumer main.c)
target_link_libraries(consumer PRIVATE akstdlib::akstdlib)

73
tests/consumer/main.c Normal file
View File

@@ -0,0 +1,73 @@
/*
* Smoke test for the installed package. See CMakeLists.txt beside this file.
*
* Deliberately touches one function from each translation unit -- the version
* entry points, a string buffer, a formatted append and a print -- so that a
* library installed with a source file missing from its link line fails here
* rather than in whatever consumer discovers it next.
*
* Returns a distinct status per step, so a CI failure names the step.
*/
#include <akstdlib.h>
int main(void)
{
akerr_ErrorContext *e = NULL;
aksl_StrBuf buf;
const char *text = NULL;
size_t len = 0;
uint32_t hash = 0;
int count = 0;
/* The header and the .so have to agree, which is the whole point of the
* version machinery: the soname catches this first in normal use, and this
* catches a hand-install that bypassed it. */
e = AKSL_VERSION_CHECK();
if ( e != NULL ) {
return 1;
}
/* src/collections.c */
e = aksl_strbuf_init(&buf, 0);
if ( e != NULL ) {
return 2;
}
e = aksl_strbuf_appendf(&buf, "libakstdlib %s, soname %s",
aksl_version_string(), aksl_version_soname());
if ( e != NULL ) {
return 3;
}
e = aksl_strbuf_cstr(&buf, &text);
if ( e != NULL ) {
return 4;
}
/* src/string.c */
e = aksl_strlen(text, &len);
if ( e != NULL ) {
return 5;
}
/* src/stdlib.c */
e = aksl_strhash_djb2(text, len, &hash);
if ( e != NULL ) {
return 6;
}
e = aksl_printf(&count, "%s (%zu bytes, djb2 %u)\n", text, len, hash);
if ( e != NULL ) {
return 7;
}
/* src/stream.c */
e = aksl_fflush(NULL);
if ( e != NULL ) {
return 8;
}
e = aksl_strbuf_free(&buf);
if ( e != NULL ) {
return 9;
}
return 0;
}

View File

@@ -0,0 +1,26 @@
/*
* NEGATIVE COMPILE TEST -- TODO.md sections 1.3 and 2.2.5.
*
* This file must NOT compile. It is built by the CTest entry
* `negative_format_mismatch` with -Werror, and that test is marked WILL_FAIL.
*
* What it asserts: the format attributes on the variadic wrappers are attached
* and effective. Going through a wrapper is exactly how a caller loses the
* compile-time format checking it would have had calling printf(3) directly --
* printf("%d", "str") is caught and an unattributed wrapper's equivalent is not.
* AKSL_PRINTF_FORMAT is what restores it, and this is what proves it is there.
*/
#include <akstdlib.h>
int main(void)
{
char buf[64];
int count = 0;
akerr_ErrorContext *raised = NULL;
/* %d against a string. This is the line that must not build. */
raised = aksl_snprintf(&count, buf, sizeof(buf), "%d", "not an int");
return raised == NULL ? 0 : 1;
}

26
tests/negative/noignore.c Normal file
View File

@@ -0,0 +1,26 @@
/*
* NEGATIVE COMPILE TEST -- TODO.md section 1.9.
*
* This file must NOT compile. It is built by the CTest entry `negative_noignore`
* with -Werror, and that test is marked WILL_FAIL, so a successful build is a
* test failure.
*
* What it asserts: AKERR_NOIGNORE is actually attached and actually effective.
* Every entry point in this library returns an error context the caller must
* look at, and the whole design rests on discarding one being hard rather than
* merely inadvisable. AKERR_NOIGNORE expands to warn_unused_result, which does
* nothing at all if it is dropped from a declaration during a refactor -- and
* nothing about the resulting library would look wrong.
*/
#include <akstdlib.h>
int main(void)
{
void *ptr = NULL;
/* Discarding the returned context. This is the line that must not build. */
aksl_malloc(32, &ptr);
return 0;
}

1139
tests/test_collections.c Normal file

File diff suppressed because it is too large Load Diff

324
tests/test_convert.c Normal file
View File

@@ -0,0 +1,324 @@
/*
* String -> number wrappers: aksl_atoi / atol / atoll / atof.
*
* TODO.md section 1.4, complete. The cases that used to live in
* tests/test_convert_strict.c -- registered as a known failure because the ato*
* family had no error channel at all (2.1.5) -- are folded back in here now that
* they pass: non-numeric input, empty input, trailing junk and overflow are all
* errors rather than silent wrong answers.
*
* The strto* family these are built on is tested in tests/test_strto.c.
*/
#include "aksl_capture.h"
#include <errno.h>
#include <limits.h>
static int test_atoi_converts_positive(void)
{
int out = -1;
AKSL_CHECK_OK(aksl_atoi("1234", &out));
AKSL_CHECK(out == 1234);
return 0;
}
static int test_atoi_converts_negative(void)
{
int out = 0;
AKSL_CHECK_OK(aksl_atoi("-42", &out));
AKSL_CHECK(out == -42);
return 0;
}
static int test_atoi_skips_leading_whitespace(void)
{
int out = 0;
AKSL_CHECK_OK(aksl_atoi(" \t 7", &out));
AKSL_CHECK(out == 7);
return 0;
}
static int test_atoi_rejects_null_arguments(void)
{
int out = 0;
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_atoi(NULL, &out),
AKERR_NULLPOINTER, "nptr=");
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_atoi("1", NULL),
AKERR_NULLPOINTER, "dest=");
return 0;
}
/*
* The whole reason 2.1.5 mattered. atoi("not a number") returned success and 0,
* so a caller could not tell a parse failure from a legitimate zero -- and
* akbasic had to write ~60 lines of its own strtoll/strtod wrapper to avoid
* turning four diagnosable BASIC errors into four wrong answers.
*/
static int test_non_numeric_input_is_a_value_error(void)
{
int out = 99;
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_atoi("not a number", &out),
AKERR_VALUE, "no digits");
/* *dest is 0 on every failure path, never left holding a partial answer. */
AKSL_CHECK(out == 0);
return 0;
}
static int test_empty_input_is_a_value_error(void)
{
int out = 99;
AKSL_CHECK_STATUS(aksl_atoi("", &out), AKERR_VALUE);
AKSL_CHECK(out == 0);
AKSL_CHECK_STATUS(aksl_atoi(" ", &out), AKERR_VALUE);
AKSL_CHECK(out == 0);
return 0;
}
static int test_trailing_junk_is_a_value_error(void)
{
int out = 99;
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_atoi("12abc", &out),
AKERR_VALUE, "trailing junk");
AKSL_CHECK(out == 0);
/* Trailing whitespace counts as junk too: the whole string must be a number. */
AKSL_CHECK_STATUS(aksl_atoi("12 ", &out), AKERR_VALUE);
return 0;
}
/*
* "0x10" through the ato* forms is base 10, so parsing stops at the 'x' and the
* rest is trailing junk. TODO.md 1.4 left this open; the answer is that the
* prefix-honouring parse is aksl_strtol(nptr, NULL, 0, &dest), and tests/
* test_strto.c holds that half.
*/
static int test_hex_literal_is_rejected_by_the_base_10_forms(void)
{
int out = 99;
AKSL_CHECK_STATUS(aksl_atoi("0x10", &out), AKERR_VALUE);
AKSL_CHECK(out == 0);
return 0;
}
static int test_overflow_is_erange(void)
{
int out = 99;
long lout = 99;
long long llout = 99;
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_atoi("99999999999999999999", &out),
ERANGE, "out of range");
AKSL_CHECK(out == 0);
AKSL_CHECK_STATUS(aksl_atol("99999999999999999999999999", &lout), ERANGE);
AKSL_CHECK(lout == 0);
AKSL_CHECK_STATUS(aksl_atoll("99999999999999999999999999", &llout), ERANGE);
AKSL_CHECK(llout == 0);
return 0;
}
/*
* aksl_atoi narrows from long to int, so a value that fits a long and not an int
* is ERANGE from the wrapper rather than from strtol. On a platform where long
* and int are the same width there is nothing to narrow and strtol has already
* caught it, so the case is skipped rather than asserted into a false pass.
*/
static int test_atoi_narrows_to_int_range(void)
{
#if LONG_MAX > INT_MAX
int out = 99;
char buf[64];
snprintf(buf, sizeof(buf), "%ld", (long)INT_MAX + 1);
AKSL_CHECK_STATUS(aksl_atoi(buf, &out), ERANGE);
AKSL_CHECK(out == 0);
snprintf(buf, sizeof(buf), "%ld", (long)INT_MIN - 1);
AKSL_CHECK_STATUS(aksl_atoi(buf, &out), ERANGE);
AKSL_CHECK(out == 0);
#else
fprintf(stderr, " (skipped: long and int are the same width here)\n");
#endif
return 0;
}
/* TODO.md 1.4: the type boundaries round-trip exactly rather than nearly. */
static int test_type_boundaries_round_trip(void)
{
char buf[64];
int iout = 0;
long lout = 0;
long long llout = 0;
snprintf(buf, sizeof(buf), "%d", INT_MAX);
AKSL_CHECK_OK(aksl_atoi(buf, &iout));
AKSL_CHECK(iout == INT_MAX);
snprintf(buf, sizeof(buf), "%d", INT_MIN);
AKSL_CHECK_OK(aksl_atoi(buf, &iout));
AKSL_CHECK(iout == INT_MIN);
snprintf(buf, sizeof(buf), "%ld", LONG_MAX);
AKSL_CHECK_OK(aksl_atol(buf, &lout));
AKSL_CHECK(lout == LONG_MAX);
snprintf(buf, sizeof(buf), "%ld", LONG_MIN);
AKSL_CHECK_OK(aksl_atol(buf, &lout));
AKSL_CHECK(lout == LONG_MIN);
snprintf(buf, sizeof(buf), "%lld", LLONG_MAX);
AKSL_CHECK_OK(aksl_atoll(buf, &llout));
AKSL_CHECK(llout == LLONG_MAX);
snprintf(buf, sizeof(buf), "%lld", LLONG_MIN);
AKSL_CHECK_OK(aksl_atoll(buf, &llout));
AKSL_CHECK(llout == LLONG_MIN);
return 0;
}
static int test_atol_converts_and_rejects_null(void)
{
long out = 0;
AKSL_CHECK_OK(aksl_atol("2147483648", &out));
AKSL_CHECK(out == 2147483648L);
AKSL_CHECK_OK(aksl_atol("-2147483648", &out));
AKSL_CHECK(out == -2147483648L);
AKSL_CHECK_STATUS(aksl_atol(NULL, &out), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_atol("1", NULL), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_atol("nope", &out), AKERR_VALUE);
return 0;
}
static int test_atoll_converts_and_rejects_null(void)
{
long long out = 0;
AKSL_CHECK_OK(aksl_atoll("9007199254740993", &out));
AKSL_CHECK(out == 9007199254740993LL);
AKSL_CHECK_OK(aksl_atoll("-9007199254740993", &out));
AKSL_CHECK(out == -9007199254740993LL);
AKSL_CHECK_STATUS(aksl_atoll(NULL, &out), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_atoll("1", NULL), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_atoll("nope", &out), AKERR_VALUE);
return 0;
}
static int test_atof_converts_and_rejects_null(void)
{
double out = 0.0;
AKSL_CHECK_OK(aksl_atof("2.5", &out));
AKSL_CHECK(out == 2.5);
AKSL_CHECK_OK(aksl_atof(" -0.125", &out));
AKSL_CHECK(out == -0.125);
AKSL_CHECK_OK(aksl_atof("1e3", &out));
AKSL_CHECK(out == 1000.0);
AKSL_CHECK_STATUS(aksl_atof(NULL, &out), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_atof("1", NULL), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_atof("nope", &out), AKERR_VALUE);
AKSL_CHECK_STATUS(aksl_atof("1.5xyz", &out), AKERR_VALUE);
return 0;
}
/*
* TODO.md 1.4 left "inf" and "nan" open. They are accepted, because strtod(3)
* accepts them and because they are exact round-trips rather than approximations
* of something else -- a caller who did not want them has a domain check to make
* that this library cannot make for it.
*/
static int test_atof_accepts_infinity_and_nan(void)
{
double out = 0.0;
AKSL_CHECK_OK(aksl_atof("inf", &out));
AKSL_CHECK(out > 0.0 && (out * 2.0) == out);
AKSL_CHECK_OK(aksl_atof("-INFINITY", &out));
AKSL_CHECK(out < 0.0 && (out * 2.0) == out);
AKSL_CHECK_OK(aksl_atof("nan", &out));
/* The only value that is not equal to itself. */
AKSL_CHECK(out != out);
return 0;
}
/* Overflow and underflow both come back as ERANGE, which is strtod's only signal. */
static int test_atof_range_errors(void)
{
double out = 1.0;
AKSL_CHECK_STATUS(aksl_atof("1e400", &out), ERANGE);
AKSL_CHECK(out == 0.0);
AKSL_CHECK_STATUS(aksl_atof("1e-400", &out), ERANGE);
AKSL_CHECK(out == 0.0);
return 0;
}
/* The wrappers hold no state: the same input converts the same way twice. */
static int test_conversions_are_repeatable(void)
{
int first = 0;
int second = 0;
AKSL_CHECK_OK(aksl_atoi("321", &first));
AKSL_CHECK_OK(aksl_atoi("321", &second));
AKSL_CHECK(first == second);
AKSL_CHECK(first == 321);
return 0;
}
/*
* errno is cleared before each wrapped call, so a failure left over from an
* earlier unrelated call cannot be reported as this one's. Set errno to
* something conspicuous first and check that a successful conversion still
* succeeds -- the old code could report a stale errno as its own status.
*/
static int test_stale_errno_does_not_leak_into_the_result(void)
{
int out = 0;
errno = ERANGE;
AKSL_CHECK_OK(aksl_atoi("5", &out));
AKSL_CHECK(out == 5);
errno = ENOMEM;
AKSL_CHECK_STATUS(aksl_atoi("junk", &out), AKERR_VALUE);
return 0;
}
int main(void)
{
int failures = 0;
akerr_init();
AKSL_RUN(failures, test_atoi_converts_positive);
AKSL_RUN(failures, test_atoi_converts_negative);
AKSL_RUN(failures, test_atoi_skips_leading_whitespace);
AKSL_RUN(failures, test_atoi_rejects_null_arguments);
AKSL_RUN(failures, test_non_numeric_input_is_a_value_error);
AKSL_RUN(failures, test_empty_input_is_a_value_error);
AKSL_RUN(failures, test_trailing_junk_is_a_value_error);
AKSL_RUN(failures, test_hex_literal_is_rejected_by_the_base_10_forms);
AKSL_RUN(failures, test_overflow_is_erange);
AKSL_RUN(failures, test_atoi_narrows_to_int_range);
AKSL_RUN(failures, test_type_boundaries_round_trip);
AKSL_RUN(failures, test_atol_converts_and_rejects_null);
AKSL_RUN(failures, test_atoll_converts_and_rejects_null);
AKSL_RUN(failures, test_atof_converts_and_rejects_null);
AKSL_RUN(failures, test_atof_accepts_infinity_and_nan);
AKSL_RUN(failures, test_atof_range_errors);
AKSL_RUN(failures, test_conversions_are_repeatable);
AKSL_RUN(failures, test_stale_errno_does_not_leak_into_the_result);
AKSL_REPORT(failures);
}

351
tests/test_format.c Normal file
View File

@@ -0,0 +1,351 @@
/*
* Formatted-output wrappers: aksl_printf / aksl_fprintf / aksl_snprintf and
* their va_list forms.
*
* TODO.md section 1.3, now complete. Each happy path asserts both halves of the
* contract -- the byte count handed back through *count and the text that
* actually landed somewhere -- and every pointer argument is checked for its
* NULL guard.
*
* Readback goes through plain libc rather than aksl_fread so that a failure here
* points at the formatted-output wrapper under test and not at the stream
* wrappers, which tests/test_stream.c covers.
*
* aksl_sprintf is gone (TODO.md 2.2.4) and aksl_snprintf takes its place, so the
* destination-overflow case that could not previously be written is here: it is
* AKERR_OUTOFBOUNDS, not the short success snprintf(3) would have reported.
*/
#include "aksl_capture.h"
#include <errno.h>
/* Read a whole file into buf and NUL-terminate. Returns bytes read, or -1. */
static long read_file(const char *path, char *buf, size_t n)
{
FILE *fp = fopen(path, "r");
size_t got = 0;
if ( fp == NULL ) {
return -1;
}
got = fread(buf, 1, n - 1, fp);
buf[got] = '\0';
if ( ferror(fp) ) {
fclose(fp);
return -1;
}
fclose(fp);
return (long)got;
}
static int test_snprintf_writes_text_and_count(void)
{
char buf[64];
int count = -1;
memset(buf, 0x00, sizeof(buf));
AKSL_CHECK_OK(aksl_snprintf(&count, buf, sizeof(buf), "%s=%d", "x", 7));
AKSL_CHECK(count == 3);
AKSL_CHECK(strcmp(buf, "x=7") == 0);
return 0;
}
static int test_snprintf_empty_format_writes_nothing(void)
{
char buf[8] = { 'z', 'z', 'z', 'z', 'z', 'z', 'z', 'z' };
int count = -1;
AKSL_CHECK_OK(aksl_snprintf(&count, buf, sizeof(buf), "%s", ""));
AKSL_CHECK(count == 0);
AKSL_CHECK(buf[0] == '\0');
return 0;
}
/*
* The case that could not be written while the wrapper was aksl_sprintf: output
* longer than the destination. snprintf(3) would truncate, NUL-terminate and
* report the length it *would* have written, leaving the caller to notice; here
* it is an error, and *count is 0 rather than the would-have-been length.
*/
static int test_snprintf_truncation_is_an_error(void)
{
char buf[8];
int count = -1;
memset(buf, 0x00, sizeof(buf));
AKSL_CHECK_STATUS_MSG_CONTAINS(
aksl_snprintf(&count, buf, sizeof(buf), "%s", "far too long for eight bytes"),
AKERR_OUTOFBOUNDS, "truncated");
AKSL_CHECK(count == 0);
return 0;
}
/* Exactly filling the buffer is not truncation; one byte more is. */
static int test_snprintf_boundary_is_exact(void)
{
char buf[8];
int count = -1;
/* 7 characters plus the NUL is exactly sizeof(buf). */
AKSL_CHECK_OK(aksl_snprintf(&count, buf, sizeof(buf), "%s", "1234567"));
AKSL_CHECK(count == 7);
AKSL_CHECK(strcmp(buf, "1234567") == 0);
count = -1;
AKSL_CHECK_STATUS(aksl_snprintf(&count, buf, sizeof(buf), "%s", "12345678"),
AKERR_OUTOFBOUNDS);
AKSL_CHECK(count == 0);
return 0;
}
static int test_snprintf_rejects_null_arguments_and_zero_size(void)
{
char buf[8];
int count = 0;
memset(buf, 0x00, sizeof(buf));
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_snprintf(NULL, buf, sizeof(buf), "x"),
AKERR_NULLPOINTER, "count=");
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_snprintf(&count, NULL, 8, "x"),
AKERR_NULLPOINTER, "str=");
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_snprintf(&count, buf, sizeof(buf), NULL),
AKERR_NULLPOINTER, "format=");
/* size 0 leaves no room even for the terminator, so there is nothing to do. */
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_snprintf(&count, buf, 0, "x"),
AKERR_VALUE, "size=0");
return 0;
}
static int test_fprintf_writes_to_stream(void)
{
char path[AKSL_TMP_MAX];
char readback[64];
FILE *fp = NULL;
int count = -1;
AKSL_CHECK(aksl_temp_file(path, sizeof(path)) == 0);
AKSL_CHECK_OK(aksl_fopen(path, "w", &fp));
AKSL_CHECK_OK(aksl_fprintf(&count, fp, "%s %d", "value", 42));
AKSL_CHECK_OK(aksl_fclose(fp));
AKSL_CHECK(count == 8);
AKSL_CHECK(read_file(path, readback, sizeof(readback)) == 8);
AKSL_CHECK(strcmp(readback, "value 42") == 0);
AKSL_CHECK(unlink(path) == 0);
return 0;
}
/*
* vfprintf on a stream opened "r" fails outright, so the wrapper reports the
* errno it saw (EBADF on glibc). *count is 0 afterwards, not vfprintf's -1:
* TODO.md 1.3 recorded the negative count as a contract gap, and this is the
* assertion that closes it.
*/
static int test_fprintf_to_read_only_stream_reports_errno(void)
{
char path[AKSL_TMP_MAX];
FILE *fp = NULL;
int count = 99;
AKSL_CHECK(aksl_temp_file(path, sizeof(path)) == 0);
AKSL_CHECK_OK(aksl_fopen(path, "r", &fp));
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_fprintf(&count, fp, "%d", 1),
EBADF, "Short write");
AKSL_CHECK(count == 0);
AKSL_CHECK_OK(aksl_fclose(fp));
AKSL_CHECK(unlink(path) == 0);
return 0;
}
static int test_fprintf_rejects_null_arguments(void)
{
int count = 0;
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_fprintf(NULL, stdout, "x"),
AKERR_NULLPOINTER, "count=");
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_fprintf(&count, NULL, "x"),
AKERR_NULLPOINTER, "stream=");
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_fprintf(&count, stdout, NULL),
AKERR_NULLPOINTER, "format=");
return 0;
}
/*
* aksl_printf writes to stdout, so stdout is pointed at a temp file for the
* duration of the call and then restored through a dup of the original
* descriptor. Nothing between the freopen and the dup2 may return early: an
* assertion there would leave stdout attached to the temp file for the rest of
* the run, and the test report itself would vanish.
*/
static int test_printf_writes_to_stdout(void)
{
char path[AKSL_TMP_MAX];
char readback[64];
akerr_ErrorContext *err = NULL;
int count = -1;
int saved = -1;
int restored = -1;
AKSL_CHECK(aksl_temp_file(path, sizeof(path)) == 0);
fflush(stdout);
saved = dup(fileno(stdout));
AKSL_CHECK(saved >= 0);
if ( freopen(path, "w", stdout) == NULL ) {
close(saved);
AKSL_CHECK(0);
}
err = aksl_printf(&count, "%s#%d", "out", 5);
fflush(stdout);
restored = dup2(saved, fileno(stdout));
close(saved);
clearerr(stdout);
AKSL_CHECK(restored >= 0);
AKSL_CHECK(aksl_take(err) == 0);
AKSL_CHECK(count == 5);
AKSL_CHECK(read_file(path, readback, sizeof(readback)) == 5);
AKSL_CHECK(strcmp(readback, "out#5") == 0);
AKSL_CHECK(unlink(path) == 0);
return 0;
}
static int test_printf_rejects_null_arguments(void)
{
int count = 0;
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_printf(NULL, "x"),
AKERR_NULLPOINTER, "count=");
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_printf(&count, NULL),
AKERR_NULLPOINTER, "format=");
return 0;
}
/*
* The allocating form. No fixed buffer means no truncation case, which is what
* makes it the right answer when the length is not knowable in advance and the
* result is too short-lived to want a whole aksl_StrBuf.
*/
static int test_asprintf_allocates_to_fit(void)
{
char *out = NULL;
int count = -1;
int i = 0;
AKSL_CHECK_OK(aksl_asprintf(&count, &out, "%s=%d", "key", 42));
AKSL_CHECK(out != NULL);
AKSL_CHECK(strcmp(out, "key=42") == 0);
AKSL_CHECK(count == 6);
AKSL_CHECK_OK(aksl_freep((void **)&out));
/* Far longer than any buffer a fixed-size wrapper would have used. */
AKSL_CHECK_OK(aksl_asprintf(&count, &out, "%01000d", 7));
AKSL_CHECK(count == 1000);
AKSL_CHECK(strlen(out) == 1000);
AKSL_CHECK_OK(aksl_freep((void **)&out));
/* An empty result is a valid empty string, not NULL. */
AKSL_CHECK_OK(aksl_asprintf(&count, &out, "%s", ""));
AKSL_CHECK(count == 0);
AKSL_CHECK(out != NULL && out[0] == '\0');
AKSL_CHECK_OK(aksl_freep((void **)&out));
/* Repeatedly, so a leak shows up under the sanitizer build. */
for ( i = 0; i < 256; i++ ) {
AKSL_CHECK_OK(aksl_asprintf(&count, &out, "iteration %d of %d", i, 256));
AKSL_CHECK((size_t)count == strlen(out));
AKSL_CHECK_OK(aksl_freep((void **)&out));
}
out = (char *)0x1;
AKSL_CHECK_STATUS(aksl_asprintf(NULL, &out, "x"), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_asprintf(&count, NULL, "x"), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_asprintf(&count, &out, NULL), AKERR_NULLPOINTER);
/* Cleared before the format is even looked at. */
AKSL_CHECK(out == NULL);
return 0;
}
/*
* The va_list forms are what the variadic ones are built on, and TODO.md 3.1
* wanted them exposed so consumers can write their own variadic wrappers. This
* is a consumer doing exactly that.
*/
static akerr_ErrorContext AKERR_NOIGNORE *consumer_wrapper(int *count, char *buf, size_t n,
const char *fmt, ...)
{
va_list args;
akerr_ErrorContext *raised = NULL;
va_start(args, fmt);
raised = aksl_vsnprintf(count, buf, n, fmt, args);
va_end(args);
return raised;
}
static int test_va_list_forms_are_usable_from_outside(void)
{
char buf[32];
int count = -1;
memset(buf, 0x00, sizeof(buf));
AKSL_CHECK_OK(consumer_wrapper(&count, buf, sizeof(buf), "%s/%d", "via", 3));
AKSL_CHECK(count == 5);
AKSL_CHECK(strcmp(buf, "via/3") == 0);
/* The error contract survives the extra layer intact. */
AKSL_CHECK_STATUS(consumer_wrapper(&count, buf, 4, "%s", "too long"),
AKERR_OUTOFBOUNDS);
AKSL_CHECK(count == 0);
return 0;
}
/*
* Regression cover for the missing va_end (TODO.md 2.1.4). Nothing here can
* assert on register-save state directly; the point is to run the variadic
* wrappers enough times, with enough arguments, that the sanitizer build has
* something to trip over.
*/
static int test_variadic_wrappers_survive_repeated_calls(void)
{
char buf[128];
int count = 0;
int i = 0;
for ( i = 0; i < 512; i++ ) {
AKSL_CHECK_OK(aksl_snprintf(&count, buf, sizeof(buf), "%d %s %ld %c %f",
i, "iteration", (long)i, 'x', (double)i));
AKSL_CHECK(count > 0);
AKSL_CHECK((size_t)count == strlen(buf));
}
return 0;
}
int main(void)
{
int failures = 0;
akerr_init();
AKSL_RUN(failures, test_snprintf_writes_text_and_count);
AKSL_RUN(failures, test_snprintf_empty_format_writes_nothing);
AKSL_RUN(failures, test_snprintf_truncation_is_an_error);
AKSL_RUN(failures, test_snprintf_boundary_is_exact);
AKSL_RUN(failures, test_snprintf_rejects_null_arguments_and_zero_size);
AKSL_RUN(failures, test_fprintf_writes_to_stream);
AKSL_RUN(failures, test_fprintf_to_read_only_stream_reports_errno);
AKSL_RUN(failures, test_fprintf_rejects_null_arguments);
AKSL_RUN(failures, test_printf_writes_to_stdout);
AKSL_RUN(failures, test_printf_rejects_null_arguments);
AKSL_RUN(failures, test_asprintf_allocates_to_fit);
AKSL_RUN(failures, test_va_list_forms_are_usable_from_outside);
AKSL_RUN(failures, test_variadic_wrappers_survive_repeated_calls);
AKSL_REPORT(failures);
}

417
tests/test_hashmap.c Normal file
View File

@@ -0,0 +1,417 @@
/*
* The fixed-capacity hash map and FNV-1a -- src/collections.c, TODO.md 3.6.
*
* "The single most obviously-missing data structure in the library", by the
* TODO's own account: akbasic needed one three times over -- variables,
* functions and labels -- and wrote ~130 lines of open-addressed table to get it.
*
* The behaviour worth testing hardest is the tombstone. Deleting a key that
* something else probed past has to leave a marker rather than an empty slot,
* because an empty slot ends a probe chain and would make the later key
* unreachable while it is still sitting there.
*/
#include "aksl_capture.h"
#define CAP 16
typedef struct SeenLog
{
int count;
char keys[CAP][AKSL_HASHMAP_MAX_KEY];
int break_at;
} SeenLog;
static akerr_ErrorContext AKERR_NOIGNORE *record_entry(const char *key, void *value, void *data)
{
SeenLog *log = NULL;
PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, key, AKERR_NULLPOINTER, "key");
FAIL_ZERO_RETURN(e, data, AKERR_NULLPOINTER, "data");
(void)value;
log = (SeenLog *)data;
if ( log->count < CAP ) {
snprintf(log->keys[log->count], AKSL_HASHMAP_MAX_KEY, "%s", key);
}
log->count += 1;
if ( log->break_at == log->count - 1 ) {
FAIL_RETURN(e, AKERR_ITERATOR_BREAK, "stop");
}
SUCCEED_RETURN(e);
}
/* ---------------------------------------------------------------------- */
/* FNV-1a */
/* ---------------------------------------------------------------------- */
/*
* The reference values are the canonical 32-bit FNV-1a ones: h = 2166136261,
* then h = (h XOR byte) * 16777619 for each byte. "a" and "foobar" are the
* vectors from the FNV reference material.
*/
static int test_fnv1a_known_answers(void)
{
uint32_t h = 0;
AKSL_CHECK_OK(aksl_strhash_fnv1a("", 0, &h));
AKSL_CHECK(h == 2166136261u);
AKSL_CHECK_OK(aksl_strhash_fnv1a("a", 1, &h));
AKSL_CHECK(h == 0xe40c292cu);
AKSL_CHECK_OK(aksl_strhash_fnv1a("foobar", 6, &h));
AKSL_CHECK(h == 0xbf9cf968u);
/* The NUL-terminated form agrees with the length-driven one. */
AKSL_CHECK_OK(aksl_strhash_fnv1a_str("foobar", &h));
AKSL_CHECK(h == 0xbf9cf968u);
AKSL_CHECK_STATUS(aksl_strhash_fnv1a(NULL, 0, &h), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strhash_fnv1a("a", 1, NULL), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strhash_fnv1a_str(NULL, &h), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strhash_fnv1a_str("a", NULL), AKERR_NULLPOINTER);
return 0;
}
/* High bytes are unsigned here for the same reason they are in djb2. */
static int test_fnv1a_high_bit_bytes_are_unsigned(void)
{
char buf[2] = { (char)0xff, (char)0xfe };
uint32_t h = 0;
uint32_t expected = 2166136261u;
expected = (expected ^ 0xffu) * 16777619u;
expected = (expected ^ 0xfeu) * 16777619u;
AKSL_CHECK_OK(aksl_strhash_fnv1a(buf, sizeof(buf), &h));
AKSL_CHECK(h == expected);
return 0;
}
/* ---------------------------------------------------------------------- */
/* Put and get */
/* ---------------------------------------------------------------------- */
static int test_put_and_get(void)
{
aksl_HashEntry slots[CAP];
aksl_HashMap map;
int a = 1;
int b = 2;
void *value = NULL;
int found = 99;
AKSL_CHECK_OK(aksl_hashmap_init(&map, slots, CAP));
AKSL_CHECK(map.count == 0);
AKSL_CHECK(map.capacity == CAP);
AKSL_CHECK_OK(aksl_hashmap_put(&map, "alpha", &a));
AKSL_CHECK_OK(aksl_hashmap_put(&map, "beta", &b));
AKSL_CHECK(map.count == 2);
AKSL_CHECK_OK(aksl_hashmap_get(&map, "alpha", &value, &found));
AKSL_CHECK(found == 1);
AKSL_CHECK(value == &a);
AKSL_CHECK_OK(aksl_hashmap_get(&map, "beta", &value, &found));
AKSL_CHECK(found == 1);
AKSL_CHECK(value == &b);
/* A key that is not there is found = 0 and success, not an error: looking
* something up and not finding it is the ordinary case for a symbol table. */
value = (void *)0x1;
AKSL_CHECK_OK(aksl_hashmap_get(&map, "gamma", &value, &found));
AKSL_CHECK(found == 0);
AKSL_CHECK(value == (void *)0x1); /* untouched when the key is absent */
/* NULL value out-param is allowed: sometimes you only want to know if it is there. */
AKSL_CHECK_OK(aksl_hashmap_get(&map, "alpha", NULL, &found));
AKSL_CHECK(found == 1);
return 0;
}
/* Putting an existing key replaces the value rather than adding a second entry. */
static int test_put_replaces_an_existing_key(void)
{
aksl_HashEntry slots[CAP];
aksl_HashMap map;
int a = 1;
int b = 2;
void *value = NULL;
int found = 0;
AKSL_CHECK_OK(aksl_hashmap_init(&map, slots, CAP));
AKSL_CHECK_OK(aksl_hashmap_put(&map, "key", &a));
AKSL_CHECK_OK(aksl_hashmap_put(&map, "key", &b));
AKSL_CHECK(map.count == 1);
AKSL_CHECK_OK(aksl_hashmap_get(&map, "key", &value, &found));
AKSL_CHECK(found == 1);
AKSL_CHECK(value == &b);
return 0;
}
/* The map copies keys into its slots, so it owns them and cannot outlive them. */
static int test_the_map_owns_its_keys(void)
{
aksl_HashEntry slots[CAP];
aksl_HashMap map;
char key[16] = "transient";
int a = 1;
int found = 0;
AKSL_CHECK_OK(aksl_hashmap_init(&map, slots, CAP));
AKSL_CHECK_OK(aksl_hashmap_put(&map, key, &a));
/* Scribble over the caller's copy of the key. */
memset(key, 'Z', sizeof(key) - 1);
AKSL_CHECK_OK(aksl_hashmap_get(&map, "transient", NULL, &found));
AKSL_CHECK(found == 1);
return 0;
}
/* ---------------------------------------------------------------------- */
/* Bounds */
/* ---------------------------------------------------------------------- */
/*
* Full is an error naming the capacity, not a silent resize. A table that
* reallocates is a table whose entry pointers move underneath anything holding
* one; this one has a worst case you can state.
*/
static int test_a_full_map_refuses_rather_than_resizing(void)
{
aksl_HashEntry slots[4];
aksl_HashMap map;
char key[16];
int i = 0;
AKSL_CHECK_OK(aksl_hashmap_init(&map, slots, 4));
for ( i = 0; i < 4; i++ ) {
snprintf(key, sizeof(key), "k%d", i);
AKSL_CHECK_OK(aksl_hashmap_put(&map, key, NULL));
}
AKSL_CHECK(map.count == 4);
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_hashmap_put(&map, "overflow", NULL),
AKERR_OUTOFBOUNDS, "does not resize");
AKSL_CHECK(map.count == 4);
/* Everything already in there is still reachable and still correct. */
for ( i = 0; i < 4; i++ ) {
int found = 0;
snprintf(key, sizeof(key), "k%d", i);
AKSL_CHECK_OK(aksl_hashmap_get(&map, key, NULL, &found));
AKSL_CHECK(found == 1);
}
return 0;
}
/*
* A key too long for a slot is refused rather than truncated. Truncation would
* silently collide two different keys that share a prefix, which is the worst
* possible failure for a symbol table -- a lookup that returns the wrong thing.
*/
static int test_an_oversized_key_is_refused(void)
{
aksl_HashEntry slots[CAP];
aksl_HashMap map;
char longkey[AKSL_HASHMAP_MAX_KEY + 8];
int found = 0;
AKSL_CHECK_OK(aksl_hashmap_init(&map, slots, CAP));
memset(longkey, 'k', sizeof(longkey) - 1);
longkey[sizeof(longkey) - 1] = '\0';
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_hashmap_put(&map, longkey, NULL),
AKERR_OUTOFBOUNDS, "AKSL_HASHMAP_MAX_KEY");
AKSL_CHECK_STATUS(aksl_hashmap_get(&map, longkey, NULL, &found), AKERR_OUTOFBOUNDS);
AKSL_CHECK_STATUS(aksl_hashmap_remove(&map, longkey, NULL), AKERR_OUTOFBOUNDS);
/* One byte short of the limit is fine: it is a limit, not an off-by-one. */
longkey[AKSL_HASHMAP_MAX_KEY - 1] = '\0';
AKSL_CHECK_OK(aksl_hashmap_put(&map, longkey, NULL));
AKSL_CHECK_OK(aksl_hashmap_get(&map, longkey, NULL, &found));
AKSL_CHECK(found == 1);
return 0;
}
/* ---------------------------------------------------------------------- */
/* Removal and tombstones */
/* ---------------------------------------------------------------------- */
static int test_remove(void)
{
aksl_HashEntry slots[CAP];
aksl_HashMap map;
int a = 1;
int found = 99;
int removed = 99;
AKSL_CHECK_OK(aksl_hashmap_init(&map, slots, CAP));
AKSL_CHECK_OK(aksl_hashmap_put(&map, "alpha", &a));
AKSL_CHECK_OK(aksl_hashmap_remove(&map, "alpha", &removed));
AKSL_CHECK(removed == 1);
AKSL_CHECK(map.count == 0);
AKSL_CHECK_OK(aksl_hashmap_get(&map, "alpha", NULL, &found));
AKSL_CHECK(found == 0);
/* Removing what is not there is success with removed = 0. */
AKSL_CHECK_OK(aksl_hashmap_remove(&map, "alpha", &removed));
AKSL_CHECK(removed == 0);
/* The out-param is optional. */
AKSL_CHECK_OK(aksl_hashmap_remove(&map, "nothing", NULL));
return 0;
}
/*
* The tombstone case, which is the one an open-addressed table gets wrong.
*
* A tiny capacity forces collisions, so several keys share a probe chain.
* Deleting one from the middle of that chain must not cut the keys behind it
* loose: if the slot were simply emptied, the probe for a later key would stop
* there and report the key missing while it was still sitting in the table.
*/
static int test_removal_does_not_break_a_probe_chain(void)
{
aksl_HashEntry slots[4];
aksl_HashMap map;
char key[16];
int found = 0;
int i = 0;
AKSL_CHECK_OK(aksl_hashmap_init(&map, slots, 4));
for ( i = 0; i < 4; i++ ) {
snprintf(key, sizeof(key), "k%d", i);
AKSL_CHECK_OK(aksl_hashmap_put(&map, key, NULL));
}
/* Take out each key in turn and confirm every remaining one is still found. */
for ( i = 0; i < 4; i++ ) {
int j = 0;
snprintf(key, sizeof(key), "k%d", i);
AKSL_CHECK_OK(aksl_hashmap_remove(&map, key, NULL));
for ( j = i + 1; j < 4; j++ ) {
snprintf(key, sizeof(key), "k%d", j);
AKSL_CHECK_OK(aksl_hashmap_get(&map, key, NULL, &found));
AKSL_CHECK(found == 1);
}
}
AKSL_CHECK(map.count == 0);
return 0;
}
/* A tombstone is reusable, so a table churned through repeatedly does not fill up. */
static int test_tombstones_are_reused(void)
{
aksl_HashEntry slots[4];
aksl_HashMap map;
char key[16];
int i = 0;
AKSL_CHECK_OK(aksl_hashmap_init(&map, slots, 4));
/* Far more insert/remove cycles than there are slots. */
for ( i = 0; i < 64; i++ ) {
snprintf(key, sizeof(key), "cycle%d", i);
AKSL_CHECK_OK(aksl_hashmap_put(&map, key, NULL));
AKSL_CHECK_OK(aksl_hashmap_remove(&map, key, NULL));
}
AKSL_CHECK(map.count == 0);
/* Still usable afterwards. */
AKSL_CHECK_OK(aksl_hashmap_put(&map, "final", NULL));
AKSL_CHECK(map.count == 1);
return 0;
}
/* ---------------------------------------------------------------------- */
/* Iteration */
/* ---------------------------------------------------------------------- */
static int test_iterate_visits_every_live_entry(void)
{
aksl_HashEntry slots[CAP];
aksl_HashMap map;
SeenLog log;
char key[16];
int i = 0;
AKSL_CHECK_OK(aksl_hashmap_init(&map, slots, CAP));
for ( i = 0; i < 5; i++ ) {
snprintf(key, sizeof(key), "k%d", i);
AKSL_CHECK_OK(aksl_hashmap_put(&map, key, NULL));
}
/* One removed, so there is a tombstone for the walk to skip. */
AKSL_CHECK_OK(aksl_hashmap_remove(&map, "k2", NULL));
memset((void *)&log, 0x00, sizeof(log));
log.break_at = -1;
AKSL_CHECK_OK(aksl_hashmap_iterate(&map, &record_entry, &log));
AKSL_CHECK(log.count == 4);
/* The break stops it, as everywhere else in this library. */
memset((void *)&log, 0x00, sizeof(log));
log.break_at = 1;
AKSL_CHECK_OK(aksl_hashmap_iterate(&map, &record_entry, &log));
AKSL_CHECK(log.count == 2);
return 0;
}
static int test_rejects_null_and_uninitialised(void)
{
aksl_HashEntry slots[CAP];
aksl_HashMap map;
aksl_HashMap empty;
SeenLog log;
int found = 0;
memset((void *)&empty, 0x00, sizeof(empty));
memset((void *)&log, 0x00, sizeof(log));
AKSL_CHECK_OK(aksl_hashmap_init(&map, slots, CAP));
AKSL_CHECK_STATUS(aksl_hashmap_init(NULL, slots, CAP), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_hashmap_init(&map, NULL, CAP), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_hashmap_init(&map, slots, 0), AKERR_VALUE);
AKSL_CHECK_STATUS(aksl_hashmap_put(NULL, "k", NULL), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_hashmap_put(&map, NULL, NULL), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_hashmap_get(NULL, "k", NULL, &found), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_hashmap_get(&map, NULL, NULL, &found), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_hashmap_get(&map, "k", NULL, NULL), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_hashmap_remove(NULL, "k", NULL), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_hashmap_remove(&map, NULL, NULL), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_hashmap_iterate(NULL, &record_entry, &log), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_hashmap_iterate(&map, NULL, &log), AKERR_NULLPOINTER);
/* A map that was never initialised is caught rather than dereferenced. */
AKSL_CHECK_STATUS(aksl_hashmap_put(&empty, "k", NULL), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_hashmap_get(&empty, "k", NULL, &found), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_hashmap_remove(&empty, "k", NULL), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_hashmap_iterate(&empty, &record_entry, &log), AKERR_NULLPOINTER);
return 0;
}
int main(void)
{
int failures = 0;
akerr_init();
AKSL_RUN(failures, test_fnv1a_known_answers);
AKSL_RUN(failures, test_fnv1a_high_bit_bytes_are_unsigned);
AKSL_RUN(failures, test_put_and_get);
AKSL_RUN(failures, test_put_replaces_an_existing_key);
AKSL_RUN(failures, test_the_map_owns_its_keys);
AKSL_RUN(failures, test_a_full_map_refuses_rather_than_resizing);
AKSL_RUN(failures, test_an_oversized_key_is_refused);
AKSL_RUN(failures, test_remove);
AKSL_RUN(failures, test_removal_does_not_break_a_probe_chain);
AKSL_RUN(failures, test_tombstones_are_reused);
AKSL_RUN(failures, test_iterate_visits_every_live_entry);
AKSL_RUN(failures, test_rejects_null_and_uninitialised);
AKSL_REPORT(failures);
}

View File

@@ -1,20 +1,20 @@
/*
* Linked-list behaviour that the library gets right today.
* Linked list -- TODO.md section 1.7, complete.
*
* The two confirmed list defects (TODO.md 2.1.1 aksl_list_append truncating the
* chain, and 2.1.2 aksl_list_iterate skipping the head) are asserted separately
* in tests/test_list_append_chain.c and tests/test_list_iterate_head.c, which
* are registered as known-failing. Everything in this file must pass.
* The two confirmed list defects are fixed, so the tests that used to live in
* tests/test_list_append_chain.c and tests/test_list_iterate_head.c are folded
* back in here: aksl_list_append builds the whole chain rather than truncating
* it at the midpoint (2.1.1), and aksl_list_iterate starts at the head rather
* than at whatever node Floyd's slow pointer happened to stop on (2.1.2).
*
* Note that the list-shape assertions here build their lists by hand rather
* than with aksl_list_append: append cannot be trusted to produce a chain
* longer than two nodes until 2.1.1 is fixed, and using it would make these
* tests fail for a reason that has nothing to do with what they are checking.
* The chain assertions build their lists with aksl_list_append now, which is the
* point -- they could not, while append was the thing under suspicion.
*/
#include "aksl_capture.h"
#define MAX_VISITS 16
#define CHAIN_LEN 5
typedef struct VisitLog
{
@@ -54,6 +54,31 @@ static akerr_ErrorContext AKERR_NOIGNORE *record_visit(aksl_ListNode *node, void
SUCCEED_RETURN(e);
}
/* ---------------------------------------------------------------------- */
/* aksl_list_node_init */
/* ---------------------------------------------------------------------- */
/*
* TODO.md 2.2.14: every caller used to have to remember to memset a node before
* its first use, and a stack node that skipped it walked straight into garbage.
*/
static int test_node_init_zeroes_the_links(void)
{
aksl_ListNode node;
int payload = 7;
memset((void *)&node, 0xff, sizeof(node));
AKSL_CHECK_OK(aksl_list_node_init(&node, &payload));
AKSL_CHECK(node.next == NULL);
AKSL_CHECK(node.prev == NULL);
AKSL_CHECK(node.data == (void *)&payload);
AKSL_CHECK_OK(aksl_list_node_init(&node, NULL));
AKSL_CHECK(node.data == NULL);
AKSL_CHECK_STATUS(aksl_list_node_init(NULL, NULL), AKERR_NULLPOINTER);
return 0;
}
/* ---------------------------------------------------------------------- */
/* aksl_list_append */
/* ---------------------------------------------------------------------- */
@@ -63,8 +88,8 @@ static int test_append_single_node(void)
aksl_ListNode head;
aksl_ListNode tail;
memset((void *)&head, 0x00, sizeof(head));
memset((void *)&tail, 0x00, sizeof(tail));
AKSL_CHECK_OK(aksl_list_node_init(&head, NULL));
AKSL_CHECK_OK(aksl_list_node_init(&tail, NULL));
AKSL_CHECK_OK(aksl_list_append(&head, &tail));
AKSL_CHECK(head.next == &tail);
@@ -74,11 +99,66 @@ static int test_append_single_node(void)
return 0;
}
/*
* The defect that made this library's list unusable: `tail` was assigned from
* Floyd's `slow` cursor *before* slow advanced, so it tracked the node behind
* the midpoint rather than the last node. Appending n1..n4 to n0 produced the
* chain "n0 -> n4" and silently dropped n1, n2 and n3. TODO.md 2.1.1.
*/
static int test_append_builds_the_whole_chain(void)
{
aksl_ListNode node[CHAIN_LEN];
aksl_ListNode *walk = NULL;
int i = 0;
for ( i = 0; i < CHAIN_LEN; i++ ) {
AKSL_CHECK_OK(aksl_list_node_init(&node[i], NULL));
}
for ( i = 1; i < CHAIN_LEN; i++ ) {
AKSL_CHECK_OK(aksl_list_append(&node[0], &node[i]));
}
/* Forward links, head to tail. */
walk = &node[0];
for ( i = 0; i < CHAIN_LEN; i++ ) {
AKSL_CHECK(walk == &node[i]);
walk = walk->next;
}
AKSL_CHECK(walk == NULL);
/* Back links, tail to head. */
walk = &node[CHAIN_LEN - 1];
for ( i = CHAIN_LEN - 1; i >= 0; i-- ) {
AKSL_CHECK(walk == &node[i]);
walk = walk->prev;
}
AKSL_CHECK(walk == NULL);
return 0;
}
/* append points obj->prev at the real tail and terminates the chain at obj. */
static int test_append_links_prev_to_the_real_tail(void)
{
aksl_ListNode node[4];
int i = 0;
for ( i = 0; i < 4; i++ ) {
AKSL_CHECK_OK(aksl_list_node_init(&node[i], NULL));
}
for ( i = 1; i < 4; i++ ) {
AKSL_CHECK_OK(aksl_list_append(&node[0], &node[i]));
AKSL_CHECK(node[i].prev == &node[i - 1]);
AKSL_CHECK(node[i].next == NULL);
AKSL_CHECK(node[i - 1].next == &node[i]);
}
return 0;
}
static int test_append_null_arguments(void)
{
aksl_ListNode node;
memset((void *)&node, 0x00, sizeof(node));
AKSL_CHECK_OK(aksl_list_node_init(&node, NULL));
AKSL_CHECK_STATUS(aksl_list_append(NULL, &node), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_list_append(&node, NULL), AKERR_NULLPOINTER);
@@ -90,8 +170,8 @@ static int test_append_detects_self_cycle(void)
aksl_ListNode head;
aksl_ListNode node;
memset((void *)&head, 0x00, sizeof(head));
memset((void *)&node, 0x00, sizeof(node));
AKSL_CHECK_OK(aksl_list_node_init(&head, NULL));
AKSL_CHECK_OK(aksl_list_node_init(&node, NULL));
head.next = &head;
AKSL_CHECK_STATUS(aksl_list_append(&head, &node), AKERR_CIRCULAR_REFERENCE);
@@ -104,9 +184,9 @@ static int test_append_detects_two_node_cycle(void)
aksl_ListNode b;
aksl_ListNode node;
memset((void *)&a, 0x00, sizeof(a));
memset((void *)&b, 0x00, sizeof(b));
memset((void *)&node, 0x00, sizeof(node));
AKSL_CHECK_OK(aksl_list_node_init(&a, NULL));
AKSL_CHECK_OK(aksl_list_node_init(&b, NULL));
AKSL_CHECK_OK(aksl_list_node_init(&node, NULL));
a.next = &b;
b.prev = &a;
b.next = &a;
@@ -115,6 +195,56 @@ static int test_append_detects_two_node_cycle(void)
return 0;
}
/* A cycle that does not include the head: a -> b -> c -> b. */
static int test_append_detects_cycle_below_the_head(void)
{
aksl_ListNode a;
aksl_ListNode b;
aksl_ListNode c;
aksl_ListNode node;
AKSL_CHECK_OK(aksl_list_node_init(&a, NULL));
AKSL_CHECK_OK(aksl_list_node_init(&b, NULL));
AKSL_CHECK_OK(aksl_list_node_init(&c, NULL));
AKSL_CHECK_OK(aksl_list_node_init(&node, NULL));
a.next = &b;
b.prev = &a;
b.next = &c;
c.prev = &b;
c.next = &b;
AKSL_CHECK_STATUS(aksl_list_append(&a, &node), AKERR_CIRCULAR_REFERENCE);
return 0;
}
/*
* TODO.md 1.7 asked for the aliasing contract to be defined. It is refusal:
* relinking a node that is already in the list would orphan everything between
* its old position and the tail, so the tail walk -- which happens anyway --
* doubles as the check.
*/
static int test_append_refuses_a_node_already_in_the_list(void)
{
aksl_ListNode node[3];
int i = 0;
for ( i = 0; i < 3; i++ ) {
AKSL_CHECK_OK(aksl_list_node_init(&node[i], NULL));
}
AKSL_CHECK_OK(aksl_list_append(&node[0], &node[1]));
AKSL_CHECK_OK(aksl_list_append(&node[0], &node[2]));
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_list_append(&node[0], &node[1]),
AKERR_VALUE, "already in this list");
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_list_append(&node[0], &node[0]),
AKERR_VALUE, "already the head");
/* The list is untouched by the refusal. */
AKSL_CHECK(node[0].next == &node[1]);
AKSL_CHECK(node[1].next == &node[2]);
AKSL_CHECK(node[2].next == NULL);
return 0;
}
/* ---------------------------------------------------------------------- */
/* aksl_list_iterate */
/* ---------------------------------------------------------------------- */
@@ -124,7 +254,7 @@ static int test_iterate_null_arguments(void)
aksl_ListNode node;
VisitLog log;
memset((void *)&node, 0x00, sizeof(node));
AKSL_CHECK_OK(aksl_list_node_init(&node, NULL));
visitlog_init(&log);
AKSL_CHECK_STATUS(aksl_list_iterate(NULL, &record_visit, &log), AKERR_NULLPOINTER);
@@ -138,7 +268,7 @@ static int test_iterate_single_node(void)
aksl_ListNode node;
VisitLog log;
memset((void *)&node, 0x00, sizeof(node));
AKSL_CHECK_OK(aksl_list_node_init(&node, NULL));
visitlog_init(&log);
AKSL_CHECK_OK(aksl_list_iterate(&node, &record_visit, &log));
@@ -147,17 +277,69 @@ static int test_iterate_single_node(void)
return 0;
}
/*
* Every node, exactly once, in order, starting at the head. The cycle check
* leaves Floyd's `slow` cursor at the list midpoint, and the visiting loop used
* to start from there -- so the whole first half of the list, head included, was
* never passed to the callback at all. TODO.md 2.1.2.
*/
static int test_iterate_visits_every_node_from_the_head(void)
{
aksl_ListNode node[CHAIN_LEN];
VisitLog log;
int i = 0;
for ( i = 0; i < CHAIN_LEN; i++ ) {
AKSL_CHECK_OK(aksl_list_node_init(&node[i], NULL));
}
for ( i = 1; i < CHAIN_LEN; i++ ) {
AKSL_CHECK_OK(aksl_list_append(&node[0], &node[i]));
}
visitlog_init(&log);
AKSL_CHECK_OK(aksl_list_iterate(&node[0], &record_visit, &log));
AKSL_CHECK(log.count == CHAIN_LEN);
for ( i = 0; i < CHAIN_LEN; i++ ) {
AKSL_CHECK(log.seen[i] == &node[i]);
}
return 0;
}
/* An even-length list too: Floyd's midpoint lands differently, the answer does not. */
static int test_iterate_visits_every_node_of_an_even_list(void)
{
aksl_ListNode node[4];
VisitLog log;
int i = 0;
for ( i = 0; i < 4; i++ ) {
AKSL_CHECK_OK(aksl_list_node_init(&node[i], NULL));
}
for ( i = 1; i < 4; i++ ) {
AKSL_CHECK_OK(aksl_list_append(&node[0], &node[i]));
}
visitlog_init(&log);
AKSL_CHECK_OK(aksl_list_iterate(&node[0], &record_visit, &log));
AKSL_CHECK(log.count == 4);
for ( i = 0; i < 4; i++ ) {
AKSL_CHECK(log.seen[i] == &node[i]);
}
return 0;
}
static int test_iterate_detects_self_cycle(void)
{
aksl_ListNode head;
VisitLog log;
memset((void *)&head, 0x00, sizeof(head));
AKSL_CHECK_OK(aksl_list_node_init(&head, NULL));
head.next = &head;
visitlog_init(&log);
AKSL_CHECK_STATUS(aksl_list_iterate(&head, &record_visit, &log),
AKERR_CIRCULAR_REFERENCE);
AKSL_CHECK(log.count == 0);
return 0;
}
@@ -167,8 +349,8 @@ static int test_iterate_detects_two_node_cycle(void)
aksl_ListNode b;
VisitLog log;
memset((void *)&a, 0x00, sizeof(a));
memset((void *)&b, 0x00, sizeof(b));
AKSL_CHECK_OK(aksl_list_node_init(&a, NULL));
AKSL_CHECK_OK(aksl_list_node_init(&b, NULL));
a.next = &b;
b.prev = &a;
b.next = &a;
@@ -179,6 +361,29 @@ static int test_iterate_detects_two_node_cycle(void)
return 0;
}
/* Tail pointing back into the middle: a -> b -> c -> b. */
static int test_iterate_detects_tail_to_middle_cycle(void)
{
aksl_ListNode a;
aksl_ListNode b;
aksl_ListNode c;
VisitLog log;
AKSL_CHECK_OK(aksl_list_node_init(&a, NULL));
AKSL_CHECK_OK(aksl_list_node_init(&b, NULL));
AKSL_CHECK_OK(aksl_list_node_init(&c, NULL));
a.next = &b;
b.prev = &a;
b.next = &c;
c.prev = &b;
c.next = &b;
visitlog_init(&log);
AKSL_CHECK_STATUS(aksl_list_iterate(&a, &record_visit, &log),
AKERR_CIRCULAR_REFERENCE);
return 0;
}
/* An error other than ITERATOR_BREAK must come back out of the iteration with
* its status and message intact. */
static int test_iterate_propagates_callback_error(void)
@@ -186,12 +391,13 @@ static int test_iterate_propagates_callback_error(void)
aksl_ListNode node;
VisitLog log;
memset((void *)&node, 0x00, sizeof(node));
AKSL_CHECK_OK(aksl_list_node_init(&node, NULL));
visitlog_init(&log);
log.fail_at = 0;
AKSL_CHECK_STATUS(aksl_list_iterate(&node, &record_visit, &log), AKERR_VALUE);
AKSL_CHECK_MSG_CONTAINS("iterator failed at visit 0");
AKSL_CHECK_STATUS_MSG_CONTAINS(
aksl_list_iterate(&node, &record_visit, &log),
AKERR_VALUE, "iterator failed at visit 0");
AKSL_CHECK(log.count == 1);
return 0;
}
@@ -202,7 +408,7 @@ static int test_iterate_break_is_not_an_error(void)
aksl_ListNode node;
VisitLog log;
memset((void *)&node, 0x00, sizeof(node));
AKSL_CHECK_OK(aksl_list_node_init(&node, NULL));
visitlog_init(&log);
log.break_at = 0;
@@ -211,6 +417,32 @@ static int test_iterate_break_is_not_an_error(void)
return 0;
}
/*
* And the count is what proves it stopped early rather than merely finishing.
* A break on the second of five nodes must leave three nodes unvisited.
*/
static int test_iterate_break_stops_at_that_node(void)
{
aksl_ListNode node[CHAIN_LEN];
VisitLog log;
int i = 0;
for ( i = 0; i < CHAIN_LEN; i++ ) {
AKSL_CHECK_OK(aksl_list_node_init(&node[i], NULL));
}
for ( i = 1; i < CHAIN_LEN; i++ ) {
AKSL_CHECK_OK(aksl_list_append(&node[0], &node[i]));
}
visitlog_init(&log);
log.break_at = 1;
AKSL_CHECK_OK(aksl_list_iterate(&node[0], &record_visit, &log));
AKSL_CHECK(log.count == 2);
AKSL_CHECK(log.seen[0] == &node[0]);
AKSL_CHECK(log.seen[1] == &node[1]);
return 0;
}
/* ---------------------------------------------------------------------- */
/* aksl_list_pop */
/* ---------------------------------------------------------------------- */
@@ -220,16 +452,16 @@ static int test_pop_middle_node(void)
aksl_ListNode a;
aksl_ListNode b;
aksl_ListNode c;
aksl_ListNode *head = &a;
memset((void *)&a, 0x00, sizeof(a));
memset((void *)&b, 0x00, sizeof(b));
memset((void *)&c, 0x00, sizeof(c));
a.next = &b;
b.prev = &a;
b.next = &c;
c.prev = &b;
AKSL_CHECK_OK(aksl_list_node_init(&a, NULL));
AKSL_CHECK_OK(aksl_list_node_init(&b, NULL));
AKSL_CHECK_OK(aksl_list_node_init(&c, NULL));
AKSL_CHECK_OK(aksl_list_append(&a, &b));
AKSL_CHECK_OK(aksl_list_append(&a, &c));
AKSL_CHECK_OK(aksl_list_pop(&b));
AKSL_CHECK_OK(aksl_list_pop(&head, &b));
AKSL_CHECK(head == &a);
AKSL_CHECK(a.next == &c);
AKSL_CHECK(c.prev == &a);
AKSL_CHECK(b.next == NULL);
@@ -237,17 +469,23 @@ static int test_pop_middle_node(void)
return 0;
}
static int test_pop_head_node(void)
/*
* TODO.md 2.2.12: popping the head used to leave the caller's own head pointer
* aimed at a node that was no longer in the list, with no way to learn the new
* one. That is what the head out-param is for, and this is the assertion.
*/
static int test_pop_head_node_moves_the_head(void)
{
aksl_ListNode a;
aksl_ListNode b;
aksl_ListNode *head = &a;
memset((void *)&a, 0x00, sizeof(a));
memset((void *)&b, 0x00, sizeof(b));
a.next = &b;
b.prev = &a;
AKSL_CHECK_OK(aksl_list_node_init(&a, NULL));
AKSL_CHECK_OK(aksl_list_node_init(&b, NULL));
AKSL_CHECK_OK(aksl_list_append(&a, &b));
AKSL_CHECK_OK(aksl_list_pop(&a));
AKSL_CHECK_OK(aksl_list_pop(&head, &a));
AKSL_CHECK(head == &b);
AKSL_CHECK(b.prev == NULL);
AKSL_CHECK(b.next == NULL);
AKSL_CHECK(a.next == NULL);
@@ -259,13 +497,14 @@ static int test_pop_tail_node(void)
{
aksl_ListNode a;
aksl_ListNode b;
aksl_ListNode *head = &a;
memset((void *)&a, 0x00, sizeof(a));
memset((void *)&b, 0x00, sizeof(b));
a.next = &b;
b.prev = &a;
AKSL_CHECK_OK(aksl_list_node_init(&a, NULL));
AKSL_CHECK_OK(aksl_list_node_init(&b, NULL));
AKSL_CHECK_OK(aksl_list_append(&a, &b));
AKSL_CHECK_OK(aksl_list_pop(&b));
AKSL_CHECK_OK(aksl_list_pop(&head, &b));
AKSL_CHECK(head == &a);
AKSL_CHECK(a.next == NULL);
AKSL_CHECK(a.prev == NULL);
AKSL_CHECK(b.next == NULL);
@@ -273,21 +512,93 @@ static int test_pop_tail_node(void)
return 0;
}
static int test_pop_only_node(void)
/* Popping the only node empties the list, and the head becomes NULL. */
static int test_pop_only_node_empties_the_list(void)
{
aksl_ListNode a;
aksl_ListNode *head = &a;
memset((void *)&a, 0x00, sizeof(a));
AKSL_CHECK_OK(aksl_list_node_init(&a, NULL));
AKSL_CHECK_OK(aksl_list_pop(&a));
AKSL_CHECK_OK(aksl_list_pop(&head, &a));
AKSL_CHECK(head == NULL);
AKSL_CHECK(a.next == NULL);
AKSL_CHECK(a.prev == NULL);
return 0;
}
static int test_pop_null_argument(void)
static int test_pop_null_arguments(void)
{
AKSL_CHECK_STATUS(aksl_list_pop(NULL), AKERR_NULLPOINTER);
aksl_ListNode a;
aksl_ListNode *head = &a;
AKSL_CHECK_OK(aksl_list_node_init(&a, NULL));
AKSL_CHECK_STATUS(aksl_list_pop(NULL, &a), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_list_pop(&head, NULL), AKERR_NULLPOINTER);
return 0;
}
/* The list is still walkable after a pop, and the popped node is gone from it. */
static int test_pop_then_iterate(void)
{
aksl_ListNode node[4];
aksl_ListNode *head = &node[0];
VisitLog log;
int i = 0;
for ( i = 0; i < 4; i++ ) {
AKSL_CHECK_OK(aksl_list_node_init(&node[i], NULL));
}
for ( i = 1; i < 4; i++ ) {
AKSL_CHECK_OK(aksl_list_append(&node[0], &node[i]));
}
AKSL_CHECK_OK(aksl_list_pop(&head, &node[2]));
visitlog_init(&log);
AKSL_CHECK_OK(aksl_list_iterate(head, &record_visit, &log));
AKSL_CHECK(log.count == 3);
AKSL_CHECK(log.seen[0] == &node[0]);
AKSL_CHECK(log.seen[1] == &node[1]);
AKSL_CHECK(log.seen[2] == &node[3]);
/* And again, this time taking the head out. */
AKSL_CHECK_OK(aksl_list_pop(&head, &node[0]));
visitlog_init(&log);
AKSL_CHECK_OK(aksl_list_iterate(head, &record_visit, &log));
AKSL_CHECK(log.count == 2);
AKSL_CHECK(log.seen[0] == &node[1]);
AKSL_CHECK(log.seen[1] == &node[3]);
return 0;
}
/*
* TODO.md 1.7's pool-accounting case. AKSL_RUN already asserts that each test
* leaves the pool as it found it; this one drives enough failures in a row to
* exhaust the pool several times over, which is where a wrapper that raises an
* error and forgets to release it shows up as an outright exhaustion rather than
* as a slow leak.
*/
static int test_error_pool_survives_a_long_run_of_failures(void)
{
aksl_ListNode a;
aksl_ListNode b;
aksl_ListNode *head = &a;
VisitLog log;
int i = 0;
AKSL_CHECK_OK(aksl_list_node_init(&a, NULL));
AKSL_CHECK_OK(aksl_list_node_init(&b, NULL));
a.next = &a;
for ( i = 0; i < AKERR_MAX_ARRAY_ERROR + 10; i++ ) {
AKSL_CHECK_STATUS(aksl_list_append(&a, &b), AKERR_CIRCULAR_REFERENCE);
AKSL_CHECK_STATUS(aksl_list_append(NULL, &b), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_list_pop(&head, NULL), AKERR_NULLPOINTER);
visitlog_init(&log);
AKSL_CHECK_STATUS(aksl_list_iterate(&a, &record_visit, &log),
AKERR_CIRCULAR_REFERENCE);
AKSL_CHECK(aksl_slots_in_use() == 0);
}
return 0;
}
@@ -297,23 +608,36 @@ int main(void)
akerr_init();
AKSL_RUN(failures, test_node_init_zeroes_the_links);
AKSL_RUN(failures, test_append_single_node);
AKSL_RUN(failures, test_append_builds_the_whole_chain);
AKSL_RUN(failures, test_append_links_prev_to_the_real_tail);
AKSL_RUN(failures, test_append_null_arguments);
AKSL_RUN(failures, test_append_detects_self_cycle);
AKSL_RUN(failures, test_append_detects_two_node_cycle);
AKSL_RUN(failures, test_append_detects_cycle_below_the_head);
AKSL_RUN(failures, test_append_refuses_a_node_already_in_the_list);
AKSL_RUN(failures, test_iterate_null_arguments);
AKSL_RUN(failures, test_iterate_single_node);
AKSL_RUN(failures, test_iterate_visits_every_node_from_the_head);
AKSL_RUN(failures, test_iterate_visits_every_node_of_an_even_list);
AKSL_RUN(failures, test_iterate_detects_self_cycle);
AKSL_RUN(failures, test_iterate_detects_two_node_cycle);
AKSL_RUN(failures, test_iterate_detects_tail_to_middle_cycle);
AKSL_RUN(failures, test_iterate_propagates_callback_error);
AKSL_RUN(failures, test_iterate_break_is_not_an_error);
AKSL_RUN(failures, test_iterate_break_stops_at_that_node);
AKSL_RUN(failures, test_pop_middle_node);
AKSL_RUN(failures, test_pop_head_node);
AKSL_RUN(failures, test_pop_head_node_moves_the_head);
AKSL_RUN(failures, test_pop_tail_node);
AKSL_RUN(failures, test_pop_only_node);
AKSL_RUN(failures, test_pop_null_argument);
AKSL_RUN(failures, test_pop_only_node_empties_the_list);
AKSL_RUN(failures, test_pop_null_arguments);
AKSL_RUN(failures, test_pop_then_iterate);
AKSL_RUN(failures, test_error_pool_survives_a_long_run_of_failures);
AKSL_REPORT(failures);
}

View File

@@ -1,59 +0,0 @@
/*
* KNOWN FAILING -- TODO.md section 2.1.1
*
* aksl_list_append conflates Floyd cycle detection with finding the tail: the
* `tail` cursor is assigned from `slow` *before* `slow` advances, so it tracks
* the node behind the list midpoint rather than the last node. Appending to any
* list of two or more nodes therefore overwrites an interior link and silently
* drops every node after it.
*
* Appending n1..n4 to n0 produces the chain "n0 -> n4"; this test asserts the
* correct "n0 -> n1 -> n2 -> n3 -> n4" and so fails until append is fixed.
* It is registered in AKSL_KNOWN_FAILING_TESTS, which marks it WILL_FAIL; when
* the fix lands, CTest will report it as unexpectedly passing, which is the
* signal to move it into AKSL_TESTS.
*/
#include "aksl_capture.h"
#define CHAIN_LEN 5
static int test_append_builds_full_chain(void)
{
aksl_ListNode node[CHAIN_LEN];
aksl_ListNode *walk = NULL;
int i = 0;
memset((void *)node, 0x00, sizeof(node));
for ( i = 1; i < CHAIN_LEN; i++ ) {
AKSL_CHECK_OK(aksl_list_append(&node[0], &node[i]));
}
/* Forward links, head to tail. */
walk = &node[0];
for ( i = 0; i < CHAIN_LEN; i++ ) {
AKSL_CHECK(walk == &node[i]);
walk = walk->next;
}
AKSL_CHECK(walk == NULL);
/* Back links, tail to head. */
walk = &node[CHAIN_LEN - 1];
for ( i = CHAIN_LEN - 1; i >= 0; i-- ) {
AKSL_CHECK(walk == &node[i]);
walk = walk->prev;
}
AKSL_CHECK(walk == NULL);
return 0;
}
int main(void)
{
int failures = 0;
akerr_init();
AKSL_RUN(failures, test_append_builds_full_chain);
AKSL_REPORT(failures);
}

View File

@@ -1,71 +0,0 @@
/*
* KNOWN FAILING -- TODO.md section 2.1.2
*
* aksl_list_iterate runs a Floyd cycle check that leaves `slow` sitting at the
* list midpoint, and then starts the visiting loop from `slow` instead of from
* `list`. Every node before the midpoint -- including the head -- is never
* passed to the callback.
*
* The list here is built by hand rather than with aksl_list_append so that this
* test fails only for the iterate defect, not for the separate append defect in
* TODO.md 2.1.1.
*
* Registered in AKSL_KNOWN_FAILING_TESTS (WILL_FAIL). When iterate is fixed,
* CTest reports this as unexpectedly passing -- move it into AKSL_TESTS then.
*/
#include "aksl_capture.h"
#define CHAIN_LEN 3
typedef struct VisitLog
{
int count;
aksl_ListNode *seen[CHAIN_LEN];
} VisitLog;
static akerr_ErrorContext AKERR_NOIGNORE *record_visit(aksl_ListNode *node, void *data)
{
VisitLog *log = NULL;
PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, node, AKERR_NULLPOINTER, "node");
FAIL_ZERO_RETURN(e, data, AKERR_NULLPOINTER, "data");
log = (VisitLog *)data;
if ( log->count < CHAIN_LEN ) {
log->seen[log->count] = node;
}
log->count += 1;
SUCCEED_RETURN(e);
}
static int test_iterate_visits_every_node_from_the_head(void)
{
aksl_ListNode node[CHAIN_LEN];
VisitLog log;
int i = 0;
memset((void *)node, 0x00, sizeof(node));
memset((void *)&log, 0x00, sizeof(log));
for ( i = 1; i < CHAIN_LEN; i++ ) {
node[i - 1].next = &node[i];
node[i].prev = &node[i - 1];
}
AKSL_CHECK_OK(aksl_list_iterate(&node[0], &record_visit, &log));
AKSL_CHECK(log.count == CHAIN_LEN);
for ( i = 0; i < CHAIN_LEN; i++ ) {
AKSL_CHECK(log.seen[i] == &node[i]);
}
return 0;
}
int main(void)
{
int failures = 0;
akerr_init();
AKSL_RUN(failures, test_iterate_visits_every_node_from_the_head);
AKSL_REPORT(failures);
}

428
tests/test_memory.c Normal file
View File

@@ -0,0 +1,428 @@
/*
* Memory wrappers -- TODO.md section 1.1, now complete, plus the additions from
* section 3.1.
*
* The three cases 1.1 left open are all pinned here: malloc(0) is AKERR_VALUE
* rather than whatever errno happened to hold when the platform's malloc(0)
* returned NULL; an allocation the system cannot satisfy reports ENOMEM and
* leaves *dst NULL rather than garbage; and overlapping memcpy is refused with
* AKERR_VALUE rather than left as undefined behaviour that usually looks fine.
*/
#include "aksl_capture.h"
#include <errno.h>
#include <stdint.h>
static int test_malloc_writes_nonnull_pointer(void)
{
void *ptr = NULL;
AKSL_CHECK_OK(aksl_malloc(32, &ptr));
AKSL_CHECK(ptr != NULL);
AKSL_CHECK_OK(aksl_free(ptr));
return 0;
}
static int test_malloc_rejects_null_destination(void)
{
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_malloc(32, NULL),
AKERR_NULLPOINTER, "dst=");
return 0;
}
/*
* TODO.md 1.1 asked for this contract to be pinned down. malloc(0) is allowed to
* return either a unique pointer or NULL, and a NULL there is not a failure and
* need not set errno -- so the old wrapper could raise an error whose status was
* 0, which every DETECT downstream reads as success while the context holds a
* pool slot. A zero-size request is refused on its own terms instead.
*/
static int test_malloc_rejects_zero_size(void)
{
void *ptr = (void *)0x1;
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_malloc(0, &ptr), AKERR_VALUE, "zero-size");
AKSL_CHECK(ptr == NULL);
return 0;
}
/*
* An allocation no system can satisfy. SIZE_MAX/2 rather than SIZE_MAX because
* some allocators reject the latter as an obvious overflow before ever
* consulting the system, and the case worth testing is the one where the request
* is plausible and the answer is still no.
*/
static int test_malloc_reports_out_of_memory(void)
{
void *ptr = (void *)0x1;
AKSL_CHECK_STATUS(aksl_malloc(SIZE_MAX / 2, &ptr), ENOMEM);
/* Never garbage on the failure path. */
AKSL_CHECK(ptr == NULL);
return 0;
}
static int test_calloc_zeroes_and_guards(void)
{
unsigned char *buf = NULL;
void *ptr = (void *)0x1;
size_t i = 0;
AKSL_CHECK_OK(aksl_calloc(16, sizeof(unsigned char), (void **)&buf));
AKSL_CHECK(buf != NULL);
for ( i = 0; i < 16; i++ ) {
AKSL_CHECK(buf[i] == 0);
}
AKSL_CHECK_OK(aksl_free(buf));
AKSL_CHECK_STATUS(aksl_calloc(16, 1, NULL), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_calloc(0, 1, &ptr), AKERR_VALUE);
AKSL_CHECK_STATUS(aksl_calloc(1, 0, &ptr), AKERR_VALUE);
return 0;
}
/*
* realloc(3)'s trap: on failure it returns NULL and the original block is *still
* valid*, so `p = realloc(p, n)` leaks it. The wrapper takes the pointer by
* reference and leaves it untouched on failure, so the caller still holds a
* pointer it can free -- which is what this asserts by freeing it.
*/
static int test_realloc_grows_and_preserves_the_old_block_on_failure(void)
{
unsigned char *buf = NULL;
void *ptr = NULL;
size_t i = 0;
AKSL_CHECK_OK(aksl_malloc(8, (void **)&buf));
for ( i = 0; i < 8; i++ ) {
buf[i] = (unsigned char)i;
}
ptr = buf;
AKSL_CHECK_OK(aksl_realloc(&ptr, 64));
buf = (unsigned char *)ptr;
AKSL_CHECK(buf != NULL);
/* The contents that fit are carried over. */
for ( i = 0; i < 8; i++ ) {
AKSL_CHECK(buf[i] == (unsigned char)i);
}
/* A request that cannot be met leaves ptr aimed at the original block. */
AKSL_CHECK_STATUS(aksl_realloc(&ptr, SIZE_MAX / 2), ENOMEM);
AKSL_CHECK(ptr == (void *)buf);
for ( i = 0; i < 8; i++ ) {
AKSL_CHECK(buf[i] == (unsigned char)i);
}
AKSL_CHECK_OK(aksl_free(ptr));
AKSL_CHECK_STATUS(aksl_realloc(NULL, 8), AKERR_NULLPOINTER);
return 0;
}
/*
* reallocarray's whole reason for existing: `realloc(p, n * size)` is a heap
* overflow waiting for an n large enough to wrap, and the multiplication is the
* place a caller does not think to look.
*/
static int test_reallocarray_checks_the_multiplication(void)
{
void *ptr = NULL;
unsigned char *buf = NULL;
size_t i = 0;
AKSL_CHECK_OK(aksl_malloc(4, &ptr));
AKSL_CHECK_OK(aksl_reallocarray(&ptr, 16, 8));
buf = (unsigned char *)ptr;
for ( i = 0; i < 128; i++ ) {
buf[i] = (unsigned char)i;
}
AKSL_CHECK_OK(aksl_free(ptr));
ptr = NULL;
/* SIZE_MAX/4 members of 8 bytes wraps; without the check this would be a
* plausible-looking small allocation followed by a very large write. */
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_reallocarray(&ptr, SIZE_MAX / 4, 8),
AKERR_OUTOFBOUNDS, "overflows size_t");
AKSL_CHECK(ptr == NULL);
AKSL_CHECK_STATUS(aksl_reallocarray(NULL, 1, 1), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_reallocarray(&ptr, 0, 1), AKERR_VALUE);
AKSL_CHECK_STATUS(aksl_reallocarray(&ptr, 1, 0), AKERR_VALUE);
return 0;
}
/*
* aligned_alloc(3) requires size to be a multiple of the alignment and the
* alignment to be a power of two. Violating either is undefined behaviour that
* usually just returns NULL, so both are checked and reported as what they are.
*/
static int test_aligned_alloc_checks_its_preconditions(void)
{
void *ptr = (void *)0x1;
AKSL_CHECK_OK(aksl_aligned_alloc(64, 128, &ptr));
AKSL_CHECK(ptr != NULL);
AKSL_CHECK(((uintptr_t)ptr % 64) == 0);
AKSL_CHECK_OK(aksl_free(ptr));
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_aligned_alloc(24, 48, &ptr),
AKERR_VALUE, "not a power of two");
AKSL_CHECK(ptr == NULL);
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_aligned_alloc(64, 100, &ptr),
AKERR_VALUE, "not a multiple of alignment");
AKSL_CHECK_STATUS(aksl_aligned_alloc(0, 64, &ptr), AKERR_VALUE);
AKSL_CHECK_STATUS(aksl_aligned_alloc(64, 0, &ptr), AKERR_VALUE);
AKSL_CHECK_STATUS(aksl_aligned_alloc(64, 128, NULL), AKERR_NULLPOINTER);
return 0;
}
/*
* posix_memalign(3) breaks the errno convention: it returns the error number and
* leaves errno alone. The wrapper reports it as a status like everything else.
*/
static int test_posix_memalign(void)
{
void *ptr = (void *)0x1;
AKSL_CHECK_OK(aksl_posix_memalign(&ptr, sizeof(void *) * 2, 100));
AKSL_CHECK(ptr != NULL);
AKSL_CHECK(((uintptr_t)ptr % (sizeof(void *) * 2)) == 0);
AKSL_CHECK_OK(aksl_free(ptr));
/* Not a multiple of sizeof(void *), which posix_memalign refuses. */
AKSL_CHECK_STATUS(aksl_posix_memalign(&ptr, 3, 100), EINVAL);
AKSL_CHECK(ptr == NULL);
AKSL_CHECK_STATUS(aksl_posix_memalign(NULL, 16, 100), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_posix_memalign(&ptr, 16, 0), AKERR_VALUE);
return 0;
}
static int test_free_rejects_null_pointer(void)
{
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_free(NULL),
AKERR_NULLPOINTER, "NULL");
return 0;
}
/* aksl_freep clears the caller's pointer, so a second free is caught. */
static int test_freep_clears_the_pointer(void)
{
void *ptr = NULL;
AKSL_CHECK_OK(aksl_malloc(16, &ptr));
AKSL_CHECK(ptr != NULL);
AKSL_CHECK_OK(aksl_freep(&ptr));
AKSL_CHECK(ptr == NULL);
/* The second attempt is now an error instead of a double free. */
AKSL_CHECK_STATUS(aksl_freep(&ptr), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_freep(NULL), AKERR_NULLPOINTER);
return 0;
}
static int test_malloc_free_round_trip(void)
{
unsigned char *buf = NULL;
size_t i = 0;
AKSL_CHECK_OK(aksl_malloc(64, (void **)&buf));
AKSL_CHECK(buf != NULL);
for ( i = 0; i < 64; i++ ) {
buf[i] = (unsigned char)i;
}
for ( i = 0; i < 64; i++ ) {
AKSL_CHECK(buf[i] == (unsigned char)i);
}
AKSL_CHECK_OK(aksl_free(buf));
return 0;
}
static int test_memset_fills_buffer(void)
{
unsigned char buf[8];
size_t i = 0;
memset((void *)buf, 0x00, sizeof(buf));
AKSL_CHECK_OK(aksl_memset(buf, 0x5a, sizeof(buf)));
for ( i = 0; i < sizeof(buf); i++ ) {
AKSL_CHECK(buf[i] == 0x5a);
}
return 0;
}
static int test_memset_zero_length_is_noop(void)
{
unsigned char buf[4] = { 1, 2, 3, 4 };
AKSL_CHECK_OK(aksl_memset(buf, 0xff, 0));
AKSL_CHECK(buf[0] == 1);
AKSL_CHECK(buf[1] == 2);
AKSL_CHECK(buf[2] == 3);
AKSL_CHECK(buf[3] == 4);
return 0;
}
static int test_memset_rejects_null_buffer(void)
{
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_memset(NULL, 0x00, 4),
AKERR_NULLPOINTER, "s=");
return 0;
}
static int test_memcpy_copies_bytes(void)
{
unsigned char src[6] = { 0, 1, 2, 3, 4, 5 };
unsigned char dst[6] = { 9, 9, 9, 9, 9, 9 };
size_t i = 0;
AKSL_CHECK_OK(aksl_memcpy(dst, src, sizeof(src)));
for ( i = 0; i < sizeof(src); i++ ) {
AKSL_CHECK(dst[i] == src[i]);
}
return 0;
}
static int test_memcpy_zero_length_is_noop(void)
{
unsigned char src[3] = { 1, 2, 3 };
unsigned char dst[3] = { 4, 5, 6 };
AKSL_CHECK_OK(aksl_memcpy(dst, src, 0));
AKSL_CHECK(dst[0] == 4);
AKSL_CHECK(dst[1] == 5);
AKSL_CHECK(dst[2] == 6);
return 0;
}
/*
* TODO.md 1.1's open question, decided: overlap is AKERR_VALUE. memcpy(3) calls
* it undefined behaviour, which in practice means "works until the day a
* compiler version or a length changes and it does not". Callers who mean to
* overlap want aksl_memmove, and the message says so.
*/
static int test_memcpy_rejects_overlapping_ranges(void)
{
unsigned char buf[8] = { 0, 1, 2, 3, 4, 5, 6, 7 };
/* Destination inside the source range. */
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_memcpy(&buf[2], &buf[0], 4),
AKERR_VALUE, "aksl_memmove");
/* Source inside the destination range. */
AKSL_CHECK_STATUS(aksl_memcpy(&buf[0], &buf[2], 4), AKERR_VALUE);
/* Adjacent but not overlapping is fine. */
AKSL_CHECK_OK(aksl_memcpy(&buf[4], &buf[0], 4));
AKSL_CHECK(buf[4] == 0 && buf[5] == 1 && buf[6] == 2 && buf[7] == 3);
return 0;
}
static int test_memmove_handles_overlap(void)
{
unsigned char buf[8] = { 0, 1, 2, 3, 4, 5, 6, 7 };
AKSL_CHECK_OK(aksl_memmove(&buf[2], &buf[0], 4));
AKSL_CHECK(buf[2] == 0 && buf[3] == 1 && buf[4] == 2 && buf[5] == 3);
AKSL_CHECK_STATUS(aksl_memmove(NULL, buf, 1), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_memmove(buf, NULL, 1), AKERR_NULLPOINTER);
return 0;
}
static int test_memcpy_rejects_null_destination(void)
{
unsigned char src[1] = { 1 };
AKSL_CHECK_STATUS(aksl_memcpy(NULL, src, sizeof(src)),
AKERR_NULLPOINTER);
return 0;
}
static int test_memcpy_rejects_null_source(void)
{
unsigned char dst[1] = { 0 };
AKSL_CHECK_STATUS(aksl_memcpy(dst, NULL, sizeof(dst)),
AKERR_NULLPOINTER);
return 0;
}
/*
* memcmp and memchr answer through an out-param, because the return value is
* already spoken for by the error context. A memchr that finds nothing is not an
* error: "absent" is an ordinary answer to that question, so *dest is NULL and
* the call succeeds.
*/
static int test_memcmp_reports_ordering(void)
{
unsigned char a[4] = { 1, 2, 3, 4 };
unsigned char b[4] = { 1, 2, 3, 4 };
unsigned char c[4] = { 1, 2, 9, 4 };
int result = 99;
AKSL_CHECK_OK(aksl_memcmp(a, b, sizeof(a), &result));
AKSL_CHECK(result == 0);
AKSL_CHECK_OK(aksl_memcmp(a, c, sizeof(a), &result));
AKSL_CHECK(result < 0);
AKSL_CHECK_OK(aksl_memcmp(c, a, sizeof(a), &result));
AKSL_CHECK(result > 0);
AKSL_CHECK_STATUS(aksl_memcmp(NULL, b, 4, &result), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_memcmp(a, NULL, 4, &result), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_memcmp(a, b, 4, NULL), AKERR_NULLPOINTER);
return 0;
}
static int test_memchr_finds_or_reports_absence(void)
{
unsigned char buf[5] = { 'a', 'b', 'c', 'd', 'e' };
void *found = NULL;
AKSL_CHECK_OK(aksl_memchr(buf, 'c', sizeof(buf), &found));
AKSL_CHECK(found == (void *)&buf[2]);
/* Not found is a successful answer of "nowhere". */
AKSL_CHECK_OK(aksl_memchr(buf, 'z', sizeof(buf), &found));
AKSL_CHECK(found == NULL);
AKSL_CHECK_STATUS(aksl_memchr(NULL, 'a', 1, &found), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_memchr(buf, 'a', 1, NULL), AKERR_NULLPOINTER);
return 0;
}
int main(void)
{
int failures = 0;
akerr_init();
AKSL_RUN(failures, test_malloc_writes_nonnull_pointer);
AKSL_RUN(failures, test_malloc_rejects_null_destination);
AKSL_RUN(failures, test_malloc_rejects_zero_size);
AKSL_RUN(failures, test_malloc_reports_out_of_memory);
AKSL_RUN(failures, test_malloc_free_round_trip);
AKSL_RUN(failures, test_calloc_zeroes_and_guards);
AKSL_RUN(failures, test_reallocarray_checks_the_multiplication);
AKSL_RUN(failures, test_aligned_alloc_checks_its_preconditions);
AKSL_RUN(failures, test_posix_memalign);
AKSL_RUN(failures, test_realloc_grows_and_preserves_the_old_block_on_failure);
AKSL_RUN(failures, test_free_rejects_null_pointer);
AKSL_RUN(failures, test_freep_clears_the_pointer);
AKSL_RUN(failures, test_memset_fills_buffer);
AKSL_RUN(failures, test_memset_zero_length_is_noop);
AKSL_RUN(failures, test_memset_rejects_null_buffer);
AKSL_RUN(failures, test_memcpy_copies_bytes);
AKSL_RUN(failures, test_memcpy_zero_length_is_noop);
AKSL_RUN(failures, test_memcpy_rejects_overlapping_ranges);
AKSL_RUN(failures, test_memcpy_rejects_null_destination);
AKSL_RUN(failures, test_memcpy_rejects_null_source);
AKSL_RUN(failures, test_memmove_handles_overlap);
AKSL_RUN(failures, test_memcmp_reports_ordering);
AKSL_RUN(failures, test_memchr_finds_or_reports_absence);
AKSL_REPORT(failures);
}

199
tests/test_path.c Normal file
View File

@@ -0,0 +1,199 @@
/*
* aksl_realpath and aksl_realpath_alloc -- TODO.md section 1.5, now complete.
*
* The happy paths compare against realpath(3) itself rather than against a
* hard-coded string, because $TMPDIR may itself be a symlink (/tmp -> /private/tmp
* and friends) and the resolved answer is what the platform says it is.
*
* The failure cases now pass an *uninitialised* resolved_path on purpose. That
* used to be the crash case (TODO.md 2.1.6): the wrapper's own error path
* formatted the buffer with %s while realpath(3) leaves its contents
* unspecified on failure, so the library read uninitialised memory while
* reporting an error. The message names only the input path now, and this test
* is what holds that -- it is meant to be run under the sanitizer build, where a
* regression is an immediate abort rather than a silent read of stack garbage.
*/
#include "aksl_capture.h"
#include <errno.h>
#include <limits.h>
static int test_resolves_an_existing_file(void)
{
char path[AKSL_TMP_MAX];
char resolved[PATH_MAX];
char expected[PATH_MAX];
AKSL_CHECK(aksl_temp_file(path, sizeof(path)) == 0);
memset(resolved, 0x00, sizeof(resolved));
AKSL_CHECK(realpath(path, expected) != NULL);
AKSL_CHECK_OK(aksl_realpath(path, resolved, sizeof(resolved)));
AKSL_CHECK(strcmp(resolved, expected) == 0);
AKSL_CHECK(resolved[0] == '/');
AKSL_CHECK(unlink(path) == 0);
return 0;
}
/* A symlink resolves to its target, not to itself. */
static int test_resolves_a_symlink_to_its_target(void)
{
char target[AKSL_TMP_MAX];
char link[AKSL_TMP_MAX];
char resolved[PATH_MAX];
char expected[PATH_MAX];
AKSL_CHECK(aksl_temp_file(target, sizeof(target)) == 0);
AKSL_CHECK(aksl_temp_file(link, sizeof(link)) == 0);
/* mkstemp created the link path as a regular file; symlink needs it gone. */
AKSL_CHECK(unlink(link) == 0);
AKSL_CHECK(symlink(target, link) == 0);
memset(resolved, 0x00, sizeof(resolved));
AKSL_CHECK(realpath(target, expected) != NULL);
AKSL_CHECK_OK(aksl_realpath(link, resolved, sizeof(resolved)));
AKSL_CHECK(strcmp(resolved, expected) == 0);
AKSL_CHECK(unlink(link) == 0);
AKSL_CHECK(unlink(target) == 0);
return 0;
}
/*
* The failure path with a buffer nobody has written to. Uninitialised on
* purpose: see the header comment. Under ASan/MSan this is the test that fails
* if the error path ever starts reading resolved_path again.
*/
static int test_missing_path_reports_enoent(void)
{
char resolved[PATH_MAX];
AKSL_CHECK_STATUS_MSG_CONTAINS(
aksl_realpath("/nonexistent/aksl/path", resolved, sizeof(resolved)),
ENOENT, "/nonexistent/aksl/path");
/* The message must name the path and must not quote the buffer back. */
AKSL_CHECK(strstr(aksl_last_message, "resolved") == NULL);
return 0;
}
/* A regular file used as a directory component is ENOTDIR, not ENOENT. */
static int test_non_directory_component_reports_enotdir(void)
{
char path[AKSL_TMP_MAX];
char child[AKSL_TMP_MAX + 8];
char resolved[PATH_MAX];
AKSL_CHECK(aksl_temp_file(path, sizeof(path)) == 0);
AKSL_CHECK((size_t)snprintf(child, sizeof(child), "%s/child", path)
< sizeof(child));
AKSL_CHECK_STATUS(aksl_realpath(child, resolved, sizeof(resolved)), ENOTDIR);
AKSL_CHECK(unlink(path) == 0);
return 0;
}
/* Two symlinks pointing at each other: the kernel gives up with ELOOP. */
static int test_symlink_loop_reports_eloop(void)
{
char a[AKSL_TMP_MAX];
char b[AKSL_TMP_MAX];
char resolved[PATH_MAX];
AKSL_CHECK(aksl_temp_file(a, sizeof(a)) == 0);
AKSL_CHECK(aksl_temp_file(b, sizeof(b)) == 0);
AKSL_CHECK(unlink(a) == 0);
AKSL_CHECK(unlink(b) == 0);
AKSL_CHECK(symlink(a, b) == 0);
AKSL_CHECK(symlink(b, a) == 0);
AKSL_CHECK_STATUS(aksl_realpath(a, resolved, sizeof(resolved)), ELOOP);
AKSL_CHECK(unlink(a) == 0);
AKSL_CHECK(unlink(b) == 0);
return 0;
}
static int test_rejects_null_arguments(void)
{
char resolved[PATH_MAX];
memset(resolved, 0x00, sizeof(resolved));
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_realpath(NULL, resolved, sizeof(resolved)),
AKERR_NULLPOINTER, "path=");
/*
* TODO.md 2.1.6: this used to be unchecked, and realpath(path, NULL)
* allocated a buffer that the wrapper then discarded and leaked.
*/
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_realpath("/tmp", NULL, PATH_MAX),
AKERR_NULLPOINTER, "resolved_path=");
return 0;
}
/*
* realpath(3) cannot be told how much room it has, so the only safe answer to an
* undersized buffer is to refuse before calling it. A caller who gets this back
* has a bug that would otherwise have been a stack smash.
*/
static int test_rejects_a_buffer_below_path_max(void)
{
char small[16];
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_realpath("/tmp", small, sizeof(small)),
AKERR_OUTOFBOUNDS, "PATH_MAX");
return 0;
}
static int test_alloc_form_resolves_and_hands_over_the_buffer(void)
{
char path[AKSL_TMP_MAX];
char expected[PATH_MAX];
char *resolved = NULL;
AKSL_CHECK(aksl_temp_file(path, sizeof(path)) == 0);
AKSL_CHECK(realpath(path, expected) != NULL);
AKSL_CHECK_OK(aksl_realpath_alloc(path, &resolved));
AKSL_CHECK(resolved != NULL);
AKSL_CHECK(strcmp(resolved, expected) == 0);
/* The buffer is the caller's; releasing it through the library closes the
* leak that the old NULL-destination path opened. */
AKSL_CHECK_OK(aksl_free(resolved));
AKSL_CHECK(unlink(path) == 0);
return 0;
}
static int test_alloc_form_reports_failure_and_writes_no_pointer(void)
{
char *resolved = (char *)0x1;
AKSL_CHECK_STATUS_MSG_CONTAINS(
aksl_realpath_alloc("/nonexistent/aksl/path", &resolved),
ENOENT, "/nonexistent/aksl/path");
/* Cleared before the call, so a failure cannot leave a stale pointer. */
AKSL_CHECK(resolved == NULL);
AKSL_CHECK_STATUS(aksl_realpath_alloc(NULL, &resolved), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_realpath_alloc("/tmp", NULL), AKERR_NULLPOINTER);
return 0;
}
int main(void)
{
int failures = 0;
akerr_init();
AKSL_RUN(failures, test_resolves_an_existing_file);
AKSL_RUN(failures, test_resolves_a_symlink_to_its_target);
AKSL_RUN(failures, test_missing_path_reports_enoent);
AKSL_RUN(failures, test_non_directory_component_reports_enotdir);
AKSL_RUN(failures, test_symlink_loop_reports_eloop);
AKSL_RUN(failures, test_rejects_null_arguments);
AKSL_RUN(failures, test_rejects_a_buffer_below_path_max);
AKSL_RUN(failures, test_alloc_form_resolves_and_hands_over_the_buffer);
AKSL_RUN(failures, test_alloc_form_reports_failure_and_writes_no_pointer);
AKSL_REPORT(failures);
}

410
tests/test_pool.c Normal file
View File

@@ -0,0 +1,410 @@
/*
* Cross-cutting properties every wrapper has to have -- TODO.md section 1.9.
*
* Two of them, and neither is about what any individual function computes:
*
* Error-pool accounting. libakerror hands out error contexts from a fixed
* process-global array of AKERR_MAX_ARRAY_ERROR slots. A wrapper that raises
* an error and forgets to release it does not fail visibly -- it fails
* AKERR_MAX_ARRAY_ERROR calls later, in whatever unrelated code happens to ask
* for a slot next, by which time the leak is nowhere near the symptom. So
* every failure path here is driven AKERR_MAX_ARRAY_ERROR + 10 times, which is
* past exhaustion several times over, with the pool checked after each round.
*
* Stack-trace content. An error that reports the wrong origin is worse than
* useless when the only debugging you get is a log file after the fact, which
* is the stated reason this library exists at all. Each assertion here names
* the function the error must say it came from and the source file it must
* name, so a FAIL that migrates into a helper is caught.
*
* AKSL_RUN already checks the pool after every test in every file. This one
* checks it inside the loop as well, because a leak of one slot per call needs
* more than one call to show up.
*/
#include "aksl_capture.h"
#include <errno.h>
#include <limits.h>
/* Rounds enough to exhaust the pool several times over. */
#define ROUNDS (AKERR_MAX_ARRAY_ERROR + 10)
/*
* Assert the last error came from `func` in a file whose name ends in `file`.
* The recorded fname is whatever __FILE__ expanded to at the FAIL site, which is
* an absolute path under CMake, so this matches on the tail rather than the
* whole thing.
*/
static int came_from(const char *func, const char *file)
{
size_t flen = strlen(file);
size_t rlen = strlen(aksl_last_file);
if ( strcmp(aksl_last_function, func) != 0 ) {
fprintf(stderr, " CHECK FAILED: error says it came from \"%s\", expected \"%s\"\n",
aksl_last_function, func);
return 1;
}
if ( rlen < flen || strcmp(aksl_last_file + (rlen - flen), file) != 0 ) {
fprintf(stderr, " CHECK FAILED: error names file \"%s\", expected one ending \"%s\"\n",
aksl_last_file, file);
return 1;
}
if ( aksl_last_line <= 0 ) {
fprintf(stderr, " CHECK FAILED: error records line %d\n", aksl_last_line);
return 1;
}
return 0;
}
/* ---------------------------------------------------------------------- */
/* Pool accounting */
/* ---------------------------------------------------------------------- */
static int test_memory_wrappers_do_not_leak_slots(void)
{
void *ptr = NULL;
int i = 0;
for ( i = 0; i < ROUNDS; i++ ) {
AKSL_CHECK_STATUS(aksl_malloc(32, NULL), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_malloc(0, &ptr), AKERR_VALUE);
AKSL_CHECK_STATUS(aksl_calloc(0, 1, &ptr), AKERR_VALUE);
AKSL_CHECK_STATUS(aksl_realloc(NULL, 8), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_free(NULL), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_freep(NULL), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_memset(NULL, 0, 1), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_memcpy(NULL, "a", 1), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_memmove(NULL, "a", 1), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_memcmp(NULL, "a", 1, NULL), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_memchr(NULL, 'a', 1, NULL), AKERR_NULLPOINTER);
AKSL_CHECK(aksl_slots_in_use() == 0);
}
return 0;
}
static int test_stream_wrappers_do_not_leak_slots(void)
{
FILE *fp = NULL;
size_t moved = 0;
int i = 0;
for ( i = 0; i < ROUNDS; i++ ) {
AKSL_CHECK_STATUS(aksl_fopen(NULL, "r", &fp), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_fopen("/nonexistent/aksl/x", "r", &fp), ENOENT);
AKSL_CHECK_STATUS(aksl_fread(NULL, 1, 1, stdin, &moved), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_fwrite(NULL, 1, 1, stdout, &moved), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_fclose(NULL), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_fseek(NULL, 0, SEEK_SET), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_ftell(NULL, NULL), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_fgetc(NULL, NULL), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_fputs(NULL, stdout), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_remove("/nonexistent/aksl/x"), ENOENT);
AKSL_CHECK(aksl_slots_in_use() == 0);
}
return 0;
}
static int test_format_wrappers_do_not_leak_slots(void)
{
char buf[16];
int count = 0;
int i = 0;
for ( i = 0; i < ROUNDS; i++ ) {
AKSL_CHECK_STATUS(aksl_printf(NULL, "x"), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_fprintf(NULL, stdout, "x"), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_snprintf(NULL, buf, sizeof(buf), "x"), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_snprintf(&count, buf, 4, "%s", "far too long"),
AKERR_OUTOFBOUNDS);
AKSL_CHECK(aksl_slots_in_use() == 0);
}
return 0;
}
static int test_conversion_wrappers_do_not_leak_slots(void)
{
int iout = 0;
long lout = 0;
unsigned long ulout = 0;
double dout = 0.0;
int i = 0;
for ( i = 0; i < ROUNDS; i++ ) {
AKSL_CHECK_STATUS(aksl_atoi(NULL, &iout), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_atoi("junk", &iout), AKERR_VALUE);
AKSL_CHECK_STATUS(aksl_atoi("99999999999999999999", &iout), ERANGE);
AKSL_CHECK_STATUS(aksl_strtol("junk", NULL, 10, &lout), AKERR_VALUE);
AKSL_CHECK_STATUS(aksl_strtoul("-1", NULL, 10, &ulout), AKERR_VALUE);
AKSL_CHECK_STATUS(aksl_strtod("junk", NULL, &dout), AKERR_VALUE);
AKSL_CHECK(aksl_slots_in_use() == 0);
}
return 0;
}
static int test_string_wrappers_do_not_leak_slots(void)
{
char buf[8];
char *at = NULL;
size_t n = 0;
int r = 0;
int i = 0;
for ( i = 0; i < ROUNDS; i++ ) {
AKSL_CHECK_STATUS(aksl_strlen(NULL, &n), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strcpy(buf, sizeof(buf), "far too long for eight"),
AKERR_OUTOFBOUNDS);
AKSL_CHECK_STATUS(aksl_strcat(buf, 0, "x"), AKERR_VALUE);
AKSL_CHECK_STATUS(aksl_strdup(NULL, &at), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strcmp(NULL, "b", &r), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strchr(NULL, 'a', &at), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strerror(0, buf, 0), AKERR_VALUE);
AKSL_CHECK(aksl_slots_in_use() == 0);
}
return 0;
}
static int test_collection_wrappers_do_not_leak_slots(void)
{
aksl_ListNode node;
aksl_ListNode *head = NULL;
aksl_List list;
aksl_TreeNode tree;
aksl_HashMap map;
aksl_HashEntry slots[4];
aksl_StrBuf strbuf;
size_t n = 0;
int i = 0;
AKSL_CHECK_OK(aksl_list_node_init(&node, NULL));
AKSL_CHECK_OK(aksl_list_init(&list));
AKSL_CHECK_OK(aksl_tree_node_init(&tree, NULL));
AKSL_CHECK_OK(aksl_hashmap_init(&map, slots, 4));
memset((void *)&strbuf, 0x00, sizeof(strbuf));
for ( i = 0; i < ROUNDS; i++ ) {
AKSL_CHECK_STATUS(aksl_list_append(NULL, &node), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_list_pop(&head, NULL), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_list_prepend(NULL, &node), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_list_length(&node, NULL), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_list_remove(&list, &node), AKERR_VALUE);
AKSL_CHECK_STATUS(aksl_tree_iterate(&tree, NULL, NULL, NULL,
AKSL_TREE_SEARCH_DFS_PREORDER, NULL),
AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_tree_count(&tree, NULL), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_hashmap_get(&map, "absent", NULL, NULL), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strbuf_append(&strbuf, "x"), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strhash_djb2(NULL, 0, NULL), AKERR_NULLPOINTER);
AKSL_CHECK(aksl_slots_in_use() == 0);
}
(void)n;
return 0;
}
/*
* A traversal that fails part-way through has the most opportunities to leak: an
* error is raised inside a callback, propagated up through several frames, and
* handled or not at the top. Drive that repeatedly too.
*/
static akerr_ErrorContext AKERR_NOIGNORE *always_fails(aksl_TreeNode *node, void *data)
{
PREPARE_ERROR(e);
(void)node;
(void)data;
FAIL_RETURN(e, AKERR_VALUE, "callback always fails");
}
static akerr_ErrorContext AKERR_NOIGNORE *always_breaks(aksl_TreeNode *node, void *data)
{
PREPARE_ERROR(e);
(void)node;
(void)data;
FAIL_RETURN(e, AKERR_ITERATOR_BREAK, "callback always breaks");
}
static int test_traversal_failures_do_not_leak_slots(void)
{
aksl_TreeNode tree[3];
int i = 0;
for ( i = 0; i < 3; i++ ) {
AKSL_CHECK_OK(aksl_tree_node_init(&tree[i], NULL));
}
tree[0].left = &tree[1];
tree[0].right = &tree[2];
for ( i = 0; i < ROUNDS; i++ ) {
/* Propagated out of the recursion. */
AKSL_CHECK_STATUS(aksl_tree_iterate(&tree[0], &always_fails, NULL, NULL,
AKSL_TREE_SEARCH_DFS_PREORDER, NULL),
AKERR_VALUE);
/* Swallowed at the top -- the released-not-returned path. */
AKSL_CHECK_OK(aksl_tree_iterate(&tree[0], &always_breaks, NULL, NULL,
AKSL_TREE_SEARCH_DFS_PREORDER, NULL));
/* And through the breadth-first walk, which also drains a queue. */
AKSL_CHECK_STATUS(aksl_tree_iterate(&tree[0], &always_fails, NULL, NULL,
AKSL_TREE_SEARCH_BFS, NULL),
AKERR_VALUE);
AKSL_CHECK_OK(aksl_tree_iterate(&tree[0], &always_breaks, NULL, NULL,
AKSL_TREE_SEARCH_BFS, NULL));
AKSL_CHECK(aksl_slots_in_use() == 0);
}
return 0;
}
/* ---------------------------------------------------------------------- */
/* Stack-trace content */
/* ---------------------------------------------------------------------- */
/*
* Each of these asserts that the error names the function that raised it and
* the file that function lives in. This is what catches a FAIL that gets moved
* into a shared helper during a refactor: the status stays right, the message
* stays right, and the origin quietly starts pointing at the wrong place.
*/
static int test_errors_name_their_origin_in_stdlib(void)
{
void *ptr = NULL;
int count = 0;
char resolved[PATH_MAX];
uint32_t h = 0;
aksl_ListNode node;
AKSL_CHECK_STATUS(aksl_malloc(32, NULL), AKERR_NULLPOINTER);
AKSL_CHECK(came_from("aksl_malloc", "src/stdlib.c") == 0);
AKSL_CHECK_STATUS(aksl_free(NULL), AKERR_NULLPOINTER);
AKSL_CHECK(came_from("aksl_free", "src/stdlib.c") == 0);
AKSL_CHECK_STATUS(aksl_memcpy(NULL, "a", 1), AKERR_NULLPOINTER);
AKSL_CHECK(came_from("aksl_memcpy", "src/stdlib.c") == 0);
AKSL_CHECK_STATUS(aksl_fopen(NULL, "r", (FILE **)&ptr), AKERR_NULLPOINTER);
AKSL_CHECK(came_from("aksl_fopen", "src/stdlib.c") == 0);
AKSL_CHECK_STATUS(aksl_fclose(NULL), AKERR_NULLPOINTER);
AKSL_CHECK(came_from("aksl_fclose", "src/stdlib.c") == 0);
/*
* aksl_printf forwards to aksl_vprintf, so the error legitimately comes
* from the latter -- the variadic form is a three-line wrapper with no FAIL
* of its own. Asserting the real origin rather than the one a reader might
* expect is the point of recording it.
*/
AKSL_CHECK_STATUS(aksl_printf(NULL, "x"), AKERR_NULLPOINTER);
AKSL_CHECK(came_from("aksl_vprintf", "src/stdlib.c") == 0);
AKSL_CHECK_STATUS(aksl_snprintf(&count, NULL, 8, "x"), AKERR_NULLPOINTER);
AKSL_CHECK(came_from("aksl_vsnprintf", "src/stdlib.c") == 0);
/* Likewise, the ato* forms are calls into the strto* ones. */
AKSL_CHECK_STATUS(aksl_atol("junk", (long *)&ptr), AKERR_VALUE);
AKSL_CHECK(came_from("aksl_strtol", "src/stdlib.c") == 0);
AKSL_CHECK_STATUS(aksl_realpath(NULL, resolved, sizeof(resolved)), AKERR_NULLPOINTER);
AKSL_CHECK(came_from("aksl_realpath", "src/stdlib.c") == 0);
AKSL_CHECK_STATUS(aksl_strhash_djb2(NULL, 0, &h), AKERR_NULLPOINTER);
AKSL_CHECK(came_from("aksl_strhash_djb2", "src/stdlib.c") == 0);
AKSL_CHECK_OK(aksl_list_node_init(&node, NULL));
AKSL_CHECK_STATUS(aksl_list_append(NULL, &node), AKERR_NULLPOINTER);
AKSL_CHECK(came_from("aksl_list_append", "src/stdlib.c") == 0);
AKSL_CHECK_STATUS(aksl_tree_iterate(NULL, NULL, NULL, NULL,
AKSL_TREE_SEARCH_DFS_PREORDER, NULL),
AKERR_NULLPOINTER);
AKSL_CHECK(came_from("aksl_tree_iterate", "src/stdlib.c") == 0);
return 0;
}
static int test_errors_name_their_origin_in_string_and_stream(void)
{
char buf[8];
size_t n = 0;
long pos = 0;
AKSL_CHECK_STATUS(aksl_strlen(NULL, &n), AKERR_NULLPOINTER);
AKSL_CHECK(came_from("aksl_strlen", "src/string.c") == 0);
AKSL_CHECK_STATUS(aksl_strcpy(buf, sizeof(buf), "far too long for eight"),
AKERR_OUTOFBOUNDS);
AKSL_CHECK(came_from("aksl_strcpy", "src/string.c") == 0);
AKSL_CHECK_STATUS(aksl_strdup(NULL, NULL), AKERR_NULLPOINTER);
AKSL_CHECK(came_from("aksl_strdup", "src/string.c") == 0);
AKSL_CHECK_STATUS(aksl_strerror(0, buf, 0), AKERR_VALUE);
AKSL_CHECK(came_from("aksl_strerror", "src/string.c") == 0);
AKSL_CHECK_STATUS(aksl_fseek(NULL, 0, SEEK_SET), AKERR_NULLPOINTER);
AKSL_CHECK(came_from("aksl_fseek", "src/stream.c") == 0);
AKSL_CHECK_STATUS(aksl_ftell(NULL, &pos), AKERR_NULLPOINTER);
AKSL_CHECK(came_from("aksl_ftell", "src/stream.c") == 0);
AKSL_CHECK_STATUS(aksl_getline(NULL, NULL, NULL, NULL), AKERR_NULLPOINTER);
AKSL_CHECK(came_from("aksl_getline", "src/stream.c") == 0);
AKSL_CHECK_STATUS(aksl_mkdtemp("no-placeholder"), AKERR_VALUE);
AKSL_CHECK(came_from("aksl_mkdtemp", "src/stream.c") == 0);
return 0;
}
static int test_errors_name_their_origin_in_collections(void)
{
aksl_ListNode node;
aksl_List list;
aksl_HashMap map;
aksl_HashEntry slots[4];
aksl_StrBuf strbuf;
char longkey[AKSL_HASHMAP_MAX_KEY + 4];
AKSL_CHECK_OK(aksl_list_node_init(&node, NULL));
AKSL_CHECK_OK(aksl_list_init(&list));
AKSL_CHECK_OK(aksl_hashmap_init(&map, slots, 4));
memset((void *)&strbuf, 0x00, sizeof(strbuf));
memset(longkey, 'k', sizeof(longkey) - 1);
longkey[sizeof(longkey) - 1] = '\0';
AKSL_CHECK_STATUS(aksl_list_prepend(NULL, &node), AKERR_NULLPOINTER);
AKSL_CHECK(came_from("aksl_list_prepend", "src/collections.c") == 0);
AKSL_CHECK_STATUS(aksl_list_remove(&list, &node), AKERR_VALUE);
AKSL_CHECK(came_from("aksl_list_remove", "src/collections.c") == 0);
AKSL_CHECK_STATUS(aksl_tree_insert(NULL, NULL, NULL), AKERR_NULLPOINTER);
AKSL_CHECK(came_from("aksl_tree_insert", "src/collections.c") == 0);
AKSL_CHECK_STATUS(aksl_hashmap_put(&map, longkey, NULL), AKERR_OUTOFBOUNDS);
AKSL_CHECK(came_from("aksl_hashmap_put", "src/collections.c") == 0);
AKSL_CHECK_STATUS(aksl_strhash_fnv1a(NULL, 0, NULL), AKERR_NULLPOINTER);
AKSL_CHECK(came_from("aksl_strhash_fnv1a", "src/collections.c") == 0);
AKSL_CHECK_STATUS(aksl_strbuf_reset(&strbuf), AKERR_NULLPOINTER);
AKSL_CHECK(came_from("aksl_strbuf_reset", "src/collections.c") == 0);
return 0;
}
int main(void)
{
int failures = 0;
akerr_init();
AKSL_RUN(failures, test_memory_wrappers_do_not_leak_slots);
AKSL_RUN(failures, test_stream_wrappers_do_not_leak_slots);
AKSL_RUN(failures, test_format_wrappers_do_not_leak_slots);
AKSL_RUN(failures, test_conversion_wrappers_do_not_leak_slots);
AKSL_RUN(failures, test_string_wrappers_do_not_leak_slots);
AKSL_RUN(failures, test_collection_wrappers_do_not_leak_slots);
AKSL_RUN(failures, test_traversal_failures_do_not_leak_slots);
AKSL_RUN(failures, test_errors_name_their_origin_in_stdlib);
AKSL_RUN(failures, test_errors_name_their_origin_in_string_and_stream);
AKSL_RUN(failures, test_errors_name_their_origin_in_collections);
AKSL_REPORT(failures);
}

View File

@@ -0,0 +1,225 @@
/*
* libakstdlib's side of the libakerror 1.0.0 status registry contract.
*
* See deps/libakerror/UPGRADING.md. That release made the status-name table
* private, moved consumer status codes to a reserved band starting at
* AKERR_FIRST_CONSUMER_STATUS, and made ownership of a range enforced rather
* than advisory. None of it broke this library's build, because libakstdlib
* defines no status codes of its own -- it raises libakerror's AKERR_* codes and
* propagates the host's errno values, both of which live in libakerror's own
* 0-255 band.
*
* That "defines none of its own" is now a contract worth pinning rather than an
* accident, because it is what lets libakstdlib sit in a process alongside any
* other libakerror consumer without a range negotiation. These tests assert it,
* and assert that the registry the codes are looked up in is actually populated
* -- a status whose name fails to register degrades to "Unknown Error" in every
* later stack trace, which is a silent loss of debuggability rather than a
* failure anyone would notice.
*
* If libakstdlib ever does define its own status codes, this file is where the
* reservation gets asserted: replace test_reserves_no_consumer_range with one
* that reserves the library's declared range and confirms its names registered.
*/
#include "aksl_capture.h"
#include <errno.h>
/*
* Every status libakstdlib raises, and where from. All of them belong to
* libakerror or to the host's errno space; none is defined here.
*/
static const struct {
int status;
const char *symbol;
const char *raised_by;
} aksl_raised_statuses[] = {
{ AKERR_NULLPOINTER, "AKERR_NULLPOINTER", "every NULL parameter guard" },
{ AKERR_IO, "AKERR_IO", "aksl_fread / aksl_fwrite short count" },
{ AKERR_EOF, "AKERR_EOF", "aksl_fread at end of stream" },
{ AKERR_ITERATOR_BREAK, "AKERR_ITERATOR_BREAK", "aksl_list_iterate / aksl_tree_iterate" },
{ AKERR_CIRCULAR_REFERENCE, "AKERR_CIRCULAR_REFERENCE", "aksl_list_append cycle guard" },
{ AKERR_NOT_IMPLEMENTED, "AKERR_NOT_IMPLEMENTED", "unsupported tree search modes" },
{ ENOENT, "ENOENT", "errno propagated by aksl_fopen" },
};
#define AKSL_RAISED_STATUS_COUNT \
((int)(sizeof(aksl_raised_statuses) / sizeof(aksl_raised_statuses[0])))
/*
* Ranges this file claims out of the consumer band. A reservation is
* process-global and there is no way to give one back, so each test function
* gets its own slice and no two overlap.
*
* +0 .. +15 test_reserves_no_consumer_range
* +16 .. +31 test_range_ownership_is_enforced
* +32 .. +47 test_naming_a_foreign_status_is_refused
*/
#define AKSL_TEST_RANGE_FREE (AKERR_FIRST_CONSUMER_STATUS + 0)
#define AKSL_TEST_RANGE_OWNED (AKERR_FIRST_CONSUMER_STATUS + 16)
#define AKSL_TEST_RANGE_FOREIGN (AKERR_FIRST_CONSUMER_STATUS + 32)
#define AKSL_TEST_RANGE_SIZE 16
#define AKSL_OWNER_A "aksl-test-a"
#define AKSL_OWNER_B "aksl-test-b"
/*
* The band boundary this library's "raises nothing of its own" claim rests on.
* Asserted rather than assumed: if libakerror ever moves the boundary, the
* status-band assertion below stops meaning what it says.
*/
static int test_consumer_band_starts_at_256(void)
{
AKSL_CHECK(AKERR_FIRST_CONSUMER_STATUS == 256);
AKSL_CHECK(AKERR_RESERVED_STATUS_COUNT == 256);
return 0;
}
/*
* Every status libakstdlib raises falls inside libakerror's reserved band, so it
* cannot collide with a consumer that allocates from 256 up.
*/
static int test_raised_statuses_are_in_the_reserved_band(void)
{
int i = 0;
for ( i = 0; i < AKSL_RAISED_STATUS_COUNT; i++ ) {
if ( aksl_raised_statuses[i].status >= AKERR_FIRST_CONSUMER_STATUS ) {
fprintf(stderr,
" CHECK FAILED: %s (%d) is outside libakerror's reserved band\n"
" raised by %s\n"
" at %s:%d\n",
aksl_raised_statuses[i].symbol,
aksl_raised_statuses[i].status,
aksl_raised_statuses[i].raised_by,
__FILE__, __LINE__);
return 1;
}
}
return 0;
}
/*
* The registry actually holds a name for each of them. This is the check that
* catches a build whose name table is too small: registration failures are
* reported at akerr_init() time, but the visible symptom afterwards is only that
* stack traces read "Unknown Error", which no other test would notice.
*/
static int test_raised_statuses_have_registered_names(void)
{
int i = 0;
char *name = NULL;
for ( i = 0; i < AKSL_RAISED_STATUS_COUNT; i++ ) {
name = akerr_name_for_status(aksl_raised_statuses[i].status, NULL);
if ( name == NULL || strcmp(name, "Unknown Error") == 0 ) {
fprintf(stderr,
" CHECK FAILED: %s (%d) has no registered name\n"
" raised by %s\n"
" reads back as \"%s\"\n"
" at %s:%d\n",
aksl_raised_statuses[i].symbol,
aksl_raised_statuses[i].status,
aksl_raised_statuses[i].raised_by,
name == NULL ? "(NULL)" : name,
__FILE__, __LINE__);
return 1;
}
}
return 0;
}
/*
* libakstdlib reserves nothing in the consumer band, so an application is free
* to allocate from AKERR_FIRST_CONSUMER_STATUS without coordinating with it.
* The library is driven first so that anything it might do lazily has happened
* before the range is claimed.
*/
static int test_reserves_no_consumer_range(void)
{
void *p = NULL;
AKSL_CHECK_OK(aksl_malloc(16, &p));
AKSL_CHECK_OK(aksl_free(p));
AKSL_CHECK_OK(akerr_reserve_status_range(AKSL_TEST_RANGE_FREE,
AKSL_TEST_RANGE_SIZE,
AKSL_OWNER_A));
return 0;
}
/*
* Ownership is enforced, which is what makes the assertion above meaningful --
* a reservation that succeeded against an already-claimed range would prove
* nothing. libakerror's own 0-255 band is refused for the same reason.
*/
static int test_range_ownership_is_enforced(void)
{
AKSL_CHECK_OK(akerr_reserve_status_range(AKSL_TEST_RANGE_OWNED,
AKSL_TEST_RANGE_SIZE,
AKSL_OWNER_A));
/* An identical reservation by the same owner is a documented no-op. */
AKSL_CHECK_OK(akerr_reserve_status_range(AKSL_TEST_RANGE_OWNED,
AKSL_TEST_RANGE_SIZE,
AKSL_OWNER_A));
/* The same range under a different owner is not. */
AKSL_CHECK_STATUS(akerr_reserve_status_range(AKSL_TEST_RANGE_OWNED,
AKSL_TEST_RANGE_SIZE,
AKSL_OWNER_B),
AKERR_STATUS_RANGE_OVERLAP);
/* Nor is any part of libakerror's own band. */
AKSL_CHECK_STATUS(akerr_reserve_status_range(AKERR_NULLPOINTER, 1, AKSL_OWNER_B),
AKERR_STATUS_RANGE_OVERLAP);
return 0;
}
/*
* Naming is enforced against the reservation too: a status nobody reserved and a
* status somebody else reserved are both refused, with different codes.
*/
static int test_naming_a_foreign_status_is_refused(void)
{
AKSL_CHECK_STATUS(akerr_register_status_name(AKSL_OWNER_A,
AKSL_TEST_RANGE_FOREIGN,
"Unreserved"),
AKERR_STATUS_NAME_UNRESERVED);
AKSL_CHECK_OK(akerr_reserve_status_range(AKSL_TEST_RANGE_FOREIGN,
AKSL_TEST_RANGE_SIZE,
AKSL_OWNER_A));
AKSL_CHECK_OK(akerr_register_status_name(AKSL_OWNER_A,
AKSL_TEST_RANGE_FOREIGN,
"Mine"));
AKSL_CHECK(strcmp(akerr_name_for_status(AKSL_TEST_RANGE_FOREIGN, NULL),
"Mine") == 0);
AKSL_CHECK_STATUS(akerr_register_status_name(AKSL_OWNER_B,
AKSL_TEST_RANGE_FOREIGN,
"Theirs"),
AKERR_STATUS_NAME_FOREIGN);
/* The refused registration left the owner's name in place. */
AKSL_CHECK(strcmp(akerr_name_for_status(AKSL_TEST_RANGE_FOREIGN, NULL),
"Mine") == 0);
return 0;
}
int main(void)
{
int failures = 0;
akerr_init();
AKSL_RUN(failures, test_consumer_band_starts_at_256);
AKSL_RUN(failures, test_raised_statuses_are_in_the_reserved_band);
AKSL_RUN(failures, test_raised_statuses_have_registered_names);
AKSL_RUN(failures, test_reserves_no_consumer_range);
AKSL_RUN(failures, test_range_ownership_is_enforced);
AKSL_RUN(failures, test_naming_a_foreign_status_is_refused);
AKSL_REPORT(failures);
}

258
tests/test_strbuf.c Normal file
View File

@@ -0,0 +1,258 @@
/*
* The growable string buffer -- src/collections.c, TODO.md 3.6.
*
* The bounded formatting wrappers are the right answer when the destination is
* a fixed buffer and no answer at all when the length is not known in advance.
* This is that answer, and the properties worth holding onto are that it always
* grows enough, always stays NUL-terminated, and always says so when it cannot.
*/
#include "aksl_capture.h"
static int test_init_and_free(void)
{
aksl_StrBuf buf;
const char *s = NULL;
AKSL_CHECK_OK(aksl_strbuf_init(&buf, 0));
AKSL_CHECK(buf.data != NULL);
AKSL_CHECK(buf.length == 0);
/* A zero request is raised to the minimum rather than refused. */
AKSL_CHECK(buf.capacity >= 32);
/* Valid as a C string immediately, with no finalise step. */
AKSL_CHECK_OK(aksl_strbuf_cstr(&buf, &s));
AKSL_CHECK(strcmp(s, "") == 0);
AKSL_CHECK_OK(aksl_strbuf_free(&buf));
AKSL_CHECK(buf.data == NULL);
AKSL_CHECK(buf.capacity == 0);
/* Freeing twice is an error rather than a double free, as aksl_freep arranges. */
AKSL_CHECK_STATUS(aksl_strbuf_free(&buf), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strbuf_init(NULL, 16), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strbuf_free(NULL), AKERR_NULLPOINTER);
return 0;
}
static int test_append_concatenates(void)
{
aksl_StrBuf buf;
const char *s = NULL;
AKSL_CHECK_OK(aksl_strbuf_init(&buf, 0));
AKSL_CHECK_OK(aksl_strbuf_append(&buf, "hello"));
AKSL_CHECK_OK(aksl_strbuf_append_char(&buf, ' '));
AKSL_CHECK_OK(aksl_strbuf_append(&buf, "world"));
AKSL_CHECK(buf.length == 11);
AKSL_CHECK_OK(aksl_strbuf_cstr(&buf, &s));
AKSL_CHECK(strcmp(s, "hello world") == 0);
/* Appending nothing is a no-op, not an error. */
AKSL_CHECK_OK(aksl_strbuf_append(&buf, ""));
AKSL_CHECK(buf.length == 11);
AKSL_CHECK_OK(aksl_strbuf_free(&buf));
return 0;
}
/*
* Growth is the whole point. Appending far past the initial capacity has to
* reallocate, and everything written before the move has to survive it.
*/
static int test_growth_preserves_the_contents(void)
{
aksl_StrBuf buf;
const char *s = NULL;
size_t initial = 0;
int i = 0;
AKSL_CHECK_OK(aksl_strbuf_init(&buf, 32));
initial = buf.capacity;
for ( i = 0; i < 1000; i++ ) {
AKSL_CHECK_OK(aksl_strbuf_append(&buf, "0123456789"));
}
AKSL_CHECK(buf.length == 10000);
AKSL_CHECK(buf.capacity > initial);
AKSL_CHECK(buf.capacity >= buf.length + 1);
AKSL_CHECK_OK(aksl_strbuf_cstr(&buf, &s));
AKSL_CHECK(strlen(s) == 10000);
/* Both ends, so a botched realloc shows up wherever it happened. */
AKSL_CHECK(strncmp(s, "0123456789", 10) == 0);
AKSL_CHECK(strcmp(s + 9990, "0123456789") == 0);
AKSL_CHECK_OK(aksl_strbuf_free(&buf));
return 0;
}
/* Bytes rather than a string, so an embedded NUL can be appended deliberately. */
static int test_append_bytes_keeps_embedded_nuls(void)
{
aksl_StrBuf buf;
const char *s = NULL;
AKSL_CHECK_OK(aksl_strbuf_init(&buf, 0));
AKSL_CHECK_OK(aksl_strbuf_append_bytes(&buf, "ab\0cd", 5));
/* length counts all five; the C string view stops at the first NUL. */
AKSL_CHECK(buf.length == 5);
AKSL_CHECK_OK(aksl_strbuf_cstr(&buf, &s));
AKSL_CHECK(strlen(s) == 2);
AKSL_CHECK(memcmp(s, "ab\0cd", 5) == 0);
/* And the terminator is still there past the end. */
AKSL_CHECK(s[5] == '\0');
AKSL_CHECK_OK(aksl_strbuf_free(&buf));
return 0;
}
/*
* Formatted append. vsnprintf is measured first and then written, so a long
* result grows the buffer rather than truncating -- which is the difference
* between this and aksl_snprintf into a fixed array.
*/
static int test_appendf_formats_and_grows(void)
{
aksl_StrBuf buf;
const char *s = NULL;
int i = 0;
AKSL_CHECK_OK(aksl_strbuf_init(&buf, 8));
AKSL_CHECK_OK(aksl_strbuf_appendf(&buf, "%s=%d", "x", 7));
AKSL_CHECK_OK(aksl_strbuf_cstr(&buf, &s));
AKSL_CHECK(strcmp(s, "x=7") == 0);
/* Far longer than the 8 bytes it started with. */
AKSL_CHECK_OK(aksl_strbuf_appendf(&buf, " %s", "and a good deal more text than that"));
AKSL_CHECK_OK(aksl_strbuf_cstr(&buf, &s));
AKSL_CHECK(strcmp(s, "x=7 and a good deal more text than that") == 0);
/* Repeated formatted appends, to exercise the measure/write pair often. */
AKSL_CHECK_OK(aksl_strbuf_reset(&buf));
for ( i = 0; i < 200; i++ ) {
AKSL_CHECK_OK(aksl_strbuf_appendf(&buf, "[%03d]", i));
}
AKSL_CHECK(buf.length == 1000);
AKSL_CHECK_OK(aksl_strbuf_cstr(&buf, &s));
AKSL_CHECK(strncmp(s, "[000][001][002]", 15) == 0);
AKSL_CHECK(strcmp(s + 995, "[199]") == 0);
AKSL_CHECK_OK(aksl_strbuf_free(&buf));
return 0;
}
/* An empty format produces nothing and is not an error. */
static int test_appendf_with_no_output(void)
{
aksl_StrBuf buf;
AKSL_CHECK_OK(aksl_strbuf_init(&buf, 0));
AKSL_CHECK_OK(aksl_strbuf_appendf(&buf, "%s", ""));
AKSL_CHECK(buf.length == 0);
AKSL_CHECK(buf.data[0] == '\0');
AKSL_CHECK_OK(aksl_strbuf_free(&buf));
return 0;
}
/* reset empties without releasing, so the capacity is reused. */
static int test_reset_keeps_the_capacity(void)
{
aksl_StrBuf buf;
const char *s = NULL;
size_t grown = 0;
AKSL_CHECK_OK(aksl_strbuf_init(&buf, 0));
AKSL_CHECK_OK(aksl_strbuf_append(&buf, "something reasonably long to force a grow"));
grown = buf.capacity;
AKSL_CHECK_OK(aksl_strbuf_reset(&buf));
AKSL_CHECK(buf.length == 0);
AKSL_CHECK(buf.capacity == grown);
AKSL_CHECK_OK(aksl_strbuf_cstr(&buf, &s));
AKSL_CHECK(strcmp(s, "") == 0);
/* And it still works afterwards. */
AKSL_CHECK_OK(aksl_strbuf_append(&buf, "again"));
AKSL_CHECK_OK(aksl_strbuf_cstr(&buf, &s));
AKSL_CHECK(strcmp(s, "again") == 0);
AKSL_CHECK_OK(aksl_strbuf_free(&buf));
return 0;
}
/*
* Every entry point checks that the buffer was initialised, so a zeroed
* aksl_StrBuf on the stack is an error rather than a NULL dereference.
*/
static int test_rejects_null_and_uninitialised(void)
{
aksl_StrBuf buf;
aksl_StrBuf zeroed;
const char *s = NULL;
memset((void *)&zeroed, 0x00, sizeof(zeroed));
AKSL_CHECK_OK(aksl_strbuf_init(&buf, 0));
AKSL_CHECK_STATUS(aksl_strbuf_append(NULL, "x"), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strbuf_append(&buf, NULL), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strbuf_append_bytes(NULL, "x", 1), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strbuf_append_bytes(&buf, NULL, 1), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strbuf_append_char(NULL, 'x'), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strbuf_appendf(NULL, "x"), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strbuf_appendf(&buf, NULL), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strbuf_reset(NULL), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strbuf_cstr(NULL, &s), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strbuf_cstr(&buf, NULL), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strbuf_append(&zeroed, "x"), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strbuf_appendf(&zeroed, "x"), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strbuf_reset(&zeroed), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strbuf_cstr(&zeroed, &s), AKERR_NULLPOINTER);
AKSL_CHECK_OK(aksl_strbuf_free(&buf));
return 0;
}
/*
* The reason this type exists, written the way a caller would: build a report of
* unknown length out of pieces, without anyone having to size a buffer first.
*/
static int test_builds_a_report(void)
{
aksl_StrBuf buf;
const char *s = NULL;
static const char *names[3] = { "alpha", "beta", "gamma" };
int i = 0;
AKSL_CHECK_OK(aksl_strbuf_init(&buf, 0));
AKSL_CHECK_OK(aksl_strbuf_append(&buf, "items:"));
for ( i = 0; i < 3; i++ ) {
AKSL_CHECK_OK(aksl_strbuf_appendf(&buf, " %s(%d)", names[i], i));
}
AKSL_CHECK_OK(aksl_strbuf_cstr(&buf, &s));
AKSL_CHECK(strcmp(s, "items: alpha(0) beta(1) gamma(2)") == 0);
AKSL_CHECK_OK(aksl_strbuf_free(&buf));
return 0;
}
int main(void)
{
int failures = 0;
akerr_init();
AKSL_RUN(failures, test_init_and_free);
AKSL_RUN(failures, test_append_concatenates);
AKSL_RUN(failures, test_growth_preserves_the_contents);
AKSL_RUN(failures, test_append_bytes_keeps_embedded_nuls);
AKSL_RUN(failures, test_appendf_formats_and_grows);
AKSL_RUN(failures, test_appendf_with_no_output);
AKSL_RUN(failures, test_reset_keeps_the_capacity);
AKSL_RUN(failures, test_rejects_null_and_uninitialised);
AKSL_RUN(failures, test_builds_a_report);
AKSL_REPORT(failures);
}

330
tests/test_stream.c Normal file
View File

@@ -0,0 +1,330 @@
/*
* Stream wrappers: aksl_fopen / aksl_fread / aksl_fwrite / aksl_fclose.
*
* TODO.md section 1.2, now complete. The happy paths, the round trip, every
* NULL guard, both stream-error statuses, the transferred-member count that
* aksl_fread and aksl_fwrite report through nmemb_out, and the three cases that
* needed a hostile file to produce: a mode-denied path (EACCES), a full device
* (/dev/full) for the short write, and a stream whose buffered flush fails at
* fclose time.
*
* The /dev/full tests are skipped where the device is absent -- it is a Linux
* thing -- rather than failing, and say so on stderr so a skip cannot be
* mistaken for a pass.
*
* Temp files come from aksl_temp_file() and are unlinked by the test that made
* them, so a failing test leaves nothing behind but the file it was mid-way
* through.
*/
#include "aksl_capture.h"
#include <errno.h>
#include <sys/stat.h>
static int test_fopen_writes_stream_pointer(void)
{
char path[AKSL_TMP_MAX];
FILE *fp = NULL;
AKSL_CHECK(aksl_temp_file(path, sizeof(path)) == 0);
AKSL_CHECK_OK(aksl_fopen(path, "w", &fp));
AKSL_CHECK(fp != NULL);
AKSL_CHECK_OK(aksl_fclose(fp));
AKSL_CHECK(unlink(path) == 0);
return 0;
}
static int test_fopen_reports_missing_path(void)
{
FILE *fp = NULL;
/* The pathname belongs in the message; the status is the errno fopen saw. */
AKSL_CHECK_STATUS_MSG_CONTAINS(
aksl_fopen("/nonexistent/aksl/stream", "r", &fp),
ENOENT, "/nonexistent/aksl/stream");
/* *fp is cleared before the call, so a failed open cannot leave garbage. */
AKSL_CHECK(fp == NULL);
return 0;
}
/*
* A file the caller cannot open for reading is EACCES rather than ENOENT.
* Skipped under a user who bypasses the permission bits -- root, or anything
* holding CAP_DAC_OVERRIDE -- where chmod 000 simply does not deny anything.
*/
static int test_fopen_reports_permission_denied(void)
{
char path[AKSL_TMP_MAX];
FILE *fp = NULL;
AKSL_CHECK(aksl_temp_file(path, sizeof(path)) == 0);
AKSL_CHECK(chmod(path, 0000) == 0);
if ( geteuid() == 0 ) {
fprintf(stderr, " (skipped: running as root, chmod 000 denies nothing)\n");
AKSL_CHECK(unlink(path) == 0);
return 0;
}
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_fopen(path, "r", &fp), EACCES, path);
AKSL_CHECK(unlink(path) == 0);
return 0;
}
static int test_fopen_rejects_null_arguments(void)
{
char path[AKSL_TMP_MAX];
FILE *fp = NULL;
AKSL_CHECK(aksl_temp_file(path, sizeof(path)) == 0);
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_fopen(path, "r", NULL),
AKERR_NULLPOINTER, "fp=");
/* TODO.md 2.2.2: both of these used to go straight through to fopen(3). */
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_fopen(NULL, "r", &fp),
AKERR_NULLPOINTER, "pathname=");
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_fopen(path, NULL, &fp),
AKERR_NULLPOINTER, "mode=");
AKSL_CHECK(unlink(path) == 0);
return 0;
}
/* fopen -> fwrite -> fclose -> fopen -> fread -> compare, all through the wrappers. */
static int test_write_read_round_trip(void)
{
char path[AKSL_TMP_MAX];
char payload[] = "libakstdlib round trip";
char readback[sizeof(payload)];
size_t moved = 0;
FILE *fp = NULL;
AKSL_CHECK(aksl_temp_file(path, sizeof(path)) == 0);
AKSL_CHECK_OK(aksl_fopen(path, "w", &fp));
AKSL_CHECK_OK(aksl_fwrite(payload, 1, sizeof(payload), fp, &moved));
AKSL_CHECK(moved == sizeof(payload));
AKSL_CHECK_OK(aksl_fclose(fp));
fp = NULL;
moved = 0;
memset(readback, 0x00, sizeof(readback));
AKSL_CHECK_OK(aksl_fopen(path, "r", &fp));
AKSL_CHECK_OK(aksl_fread(readback, 1, sizeof(readback), fp, &moved));
AKSL_CHECK(moved == sizeof(readback));
AKSL_CHECK_OK(aksl_fclose(fp));
AKSL_CHECK(memcmp(payload, readback, sizeof(payload)) == 0);
AKSL_CHECK(unlink(path) == 0);
return 0;
}
/*
* Asking for more members than the file holds sets feof, which is AKERR_EOF --
* and nmemb_out says how many did arrive, which is the entire reason for
* distinguishing EOF from an error rather than reporting both as AKERR_IO.
*/
static int test_fread_short_read_is_eof_and_reports_the_count(void)
{
char path[AKSL_TMP_MAX];
char buf[32];
size_t moved = 0;
FILE *fp = NULL;
AKSL_CHECK(aksl_temp_file(path, sizeof(path)) == 0);
AKSL_CHECK_OK(aksl_fopen(path, "w", &fp));
AKSL_CHECK_OK(aksl_fwrite("abcd", 1, 4, fp, &moved));
AKSL_CHECK(moved == 4);
AKSL_CHECK_OK(aksl_fclose(fp));
fp = NULL;
moved = 99;
memset(buf, 0x00, sizeof(buf));
AKSL_CHECK_OK(aksl_fopen(path, "r", &fp));
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_fread(buf, 1, sizeof(buf), fp, &moved),
AKERR_EOF, "EOF");
/* TODO.md 2.2.3: this is what the caller could not previously find out. */
AKSL_CHECK(moved == 4);
AKSL_CHECK_OK(aksl_fclose(fp));
AKSL_CHECK(memcmp(buf, "abcd", 4) == 0);
AKSL_CHECK(unlink(path) == 0);
return 0;
}
/*
* A stream opened "w" has no read permission, so fread sets the error indicator
* rather than the EOF one -- and the status is the errno the C library actually
* saw, EBADF, rather than a flat AKERR_IO.
*
* That is a change from the old wrapper, which read ferror() and reported
* AKERR_IO without ever consulting errno. Routing it through AKSL_ERRNO_OR
* (TODO.md 2.2.1) keeps AKERR_IO as the fallback for the case where the stream
* is in error and errno says nothing, and hands back the real reason otherwise.
*/
static int test_fread_from_write_only_stream_reports_errno(void)
{
char path[AKSL_TMP_MAX];
char buf[4];
size_t moved = 99;
FILE *fp = NULL;
AKSL_CHECK(aksl_temp_file(path, sizeof(path)) == 0);
AKSL_CHECK_OK(aksl_fopen(path, "w", &fp));
memset(buf, 0x00, sizeof(buf));
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_fread(buf, 1, sizeof(buf), fp, &moved),
EBADF, "of 4 members");
AKSL_CHECK(moved == 0);
AKSL_CHECK_OK(aksl_fclose(fp));
AKSL_CHECK(unlink(path) == 0);
return 0;
}
static int test_fread_rejects_null_arguments(void)
{
char path[AKSL_TMP_MAX];
char buf[4];
size_t moved = 0;
FILE *fp = NULL;
memset(buf, 0x00, sizeof(buf));
AKSL_CHECK(aksl_temp_file(path, sizeof(path)) == 0);
AKSL_CHECK_OK(aksl_fopen(path, "r", &fp));
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_fread(buf, 1, sizeof(buf), NULL, &moved),
AKERR_NULLPOINTER, "fp=");
/* TODO.md 2.2.3: ptr was never checked in either direction. */
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_fread(NULL, 1, sizeof(buf), fp, &moved),
AKERR_NULLPOINTER, "ptr=");
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_fread(buf, 1, sizeof(buf), fp, NULL),
AKERR_NULLPOINTER, "nmemb_out=");
AKSL_CHECK_OK(aksl_fclose(fp));
AKSL_CHECK(unlink(path) == 0);
return 0;
}
/* Mirror image of the fread case: a "r" stream cannot be written to. */
static int test_fwrite_to_read_only_stream_reports_errno(void)
{
char path[AKSL_TMP_MAX];
size_t moved = 99;
FILE *fp = NULL;
AKSL_CHECK(aksl_temp_file(path, sizeof(path)) == 0);
AKSL_CHECK_OK(aksl_fopen(path, "r", &fp));
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_fwrite("xy", 1, 2, fp, &moved),
EBADF, "wrote 0 of 2 members");
AKSL_CHECK(moved == 0);
AKSL_CHECK_OK(aksl_fclose(fp));
AKSL_CHECK(unlink(path) == 0);
return 0;
}
static int test_fwrite_rejects_null_arguments(void)
{
size_t moved = 0;
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_fwrite("xy", 1, 2, NULL, &moved),
AKERR_NULLPOINTER, "fp=");
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_fwrite(NULL, 1, 2, stdout, &moved),
AKERR_NULLPOINTER, "ptr=");
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_fwrite("xy", 1, 2, stdout, NULL),
AKERR_NULLPOINTER, "nmemb_out=");
return 0;
}
/*
* /dev/full accepts an open for writing and then fails every write with ENOSPC.
* It is the only convenient way to reach a genuinely short write on a stream
* that is otherwise perfectly healthy.
*/
static int test_fwrite_to_full_device_reports_enospc(void)
{
char payload[8192];
size_t moved = 99;
FILE *fp = NULL;
if ( access("/dev/full", W_OK) != 0 ) {
fprintf(stderr, " (skipped: no writable /dev/full on this platform)\n");
return 0;
}
memset(payload, 'x', sizeof(payload));
AKSL_CHECK_OK(aksl_fopen("/dev/full", "w", &fp));
/*
* Larger than any plausible stdio buffer, so the write reaches the device
* inside fwrite rather than being deferred to fclose -- the deferred case is
* the next test.
*/
AKSL_CHECK_STATUS(aksl_fwrite(payload, 1, sizeof(payload), fp, &moved), ENOSPC);
AKSL_CHECK(moved < sizeof(payload));
/*
* fclose succeeds here, and that is not a contradiction: the write already
* reached the device and failed, so by close time there is nothing left
* buffered to fail on. The deferred case -- where the write fits in the
* stdio buffer and only fclose finds out -- is the next test. Take the
* context however it comes back rather than asserting either way, since
* whether the error indicator survives to fclose is a stdio implementation
* detail and not part of this library's contract.
*/
(void)aksl_take(aksl_fclose(fp));
return 0;
}
/*
* The write that fits in the stdio buffer succeeds, and the failure surfaces
* only when fclose flushes it. A caller who checks fwrite and ignores fclose
* loses the data silently, which is why aksl_fclose reports errno rather than
* just a non-zero return.
*/
static int test_fclose_surfaces_a_failed_flush(void)
{
size_t moved = 0;
FILE *fp = NULL;
if ( access("/dev/full", W_OK) != 0 ) {
fprintf(stderr, " (skipped: no writable /dev/full on this platform)\n");
return 0;
}
AKSL_CHECK_OK(aksl_fopen("/dev/full", "w", &fp));
AKSL_CHECK_OK(aksl_fwrite("small", 1, 5, fp, &moved));
AKSL_CHECK(moved == 5);
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_fclose(fp), ENOSPC, "buffered data is lost");
return 0;
}
static int test_fclose_rejects_null_stream(void)
{
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_fclose(NULL),
AKERR_NULLPOINTER, "NULL");
return 0;
}
int main(void)
{
int failures = 0;
akerr_init();
AKSL_RUN(failures, test_fopen_writes_stream_pointer);
AKSL_RUN(failures, test_fopen_reports_missing_path);
AKSL_RUN(failures, test_fopen_reports_permission_denied);
AKSL_RUN(failures, test_fopen_rejects_null_arguments);
AKSL_RUN(failures, test_write_read_round_trip);
AKSL_RUN(failures, test_fread_short_read_is_eof_and_reports_the_count);
AKSL_RUN(failures, test_fread_from_write_only_stream_reports_errno);
AKSL_RUN(failures, test_fread_rejects_null_arguments);
AKSL_RUN(failures, test_fwrite_to_read_only_stream_reports_errno);
AKSL_RUN(failures, test_fwrite_rejects_null_arguments);
AKSL_RUN(failures, test_fwrite_to_full_device_reports_enospc);
AKSL_RUN(failures, test_fclose_surfaces_a_failed_flush);
AKSL_RUN(failures, test_fclose_rejects_null_stream);
AKSL_REPORT(failures);
}

710
tests/test_streamio.c Normal file
View File

@@ -0,0 +1,710 @@
/*
* Stream wrappers beyond open/read/write/close -- src/stream.c, TODO.md 3.1.
*
* tests/test_stream.c covers fopen/fread/fwrite/fclose. This one covers
* positioning, flushing, character and line I/O, stream state, and formatted
* input.
*
* The recurring assertion is the one the section exists for: where stdio folds
* "no more data" and "something broke" into a single sentinel -- EOF from
* fgetc, NULL from fgets, -1L from ftell -- the wrapper separates them, so
* AKERR_EOF ends a read loop and anything else is a fault.
*/
#include "aksl_capture.h"
#include <errno.h>
#include <fcntl.h>
/* A temp file containing exactly `text`, opened for reading. */
static int open_with_text(const char *text, char *path, size_t pathlen, FILE **fp)
{
FILE *out = NULL;
if ( aksl_temp_file(path, pathlen) != 0 ) {
return 1;
}
out = fopen(path, "w");
if ( out == NULL ) {
return 1;
}
if ( text[0] != '\0' && fputs(text, out) == EOF ) {
fclose(out);
return 1;
}
if ( fclose(out) != 0 ) {
return 1;
}
*fp = fopen(path, "r");
return (*fp == NULL) ? 1 : 0;
}
/* ---------------------------------------------------------------------- */
/* Positioning */
/* ---------------------------------------------------------------------- */
static int test_seek_and_tell(void)
{
char path[AKSL_TMP_MAX];
FILE *fp = NULL;
long pos = -1;
int c = 0;
AKSL_CHECK(open_with_text("0123456789", path, sizeof(path), &fp) == 0);
AKSL_CHECK_OK(aksl_ftell(fp, &pos));
AKSL_CHECK(pos == 0);
AKSL_CHECK_OK(aksl_fseek(fp, 4, SEEK_SET));
AKSL_CHECK_OK(aksl_ftell(fp, &pos));
AKSL_CHECK(pos == 4);
AKSL_CHECK_OK(aksl_fgetc(fp, &c));
AKSL_CHECK(c == '4');
AKSL_CHECK_OK(aksl_fseek(fp, -1, SEEK_END));
AKSL_CHECK_OK(aksl_fgetc(fp, &c));
AKSL_CHECK(c == '9');
/* rewind is the fseek whose failure rewind(3) throws away. */
AKSL_CHECK_OK(aksl_rewind(fp));
AKSL_CHECK_OK(aksl_ftell(fp, &pos));
AKSL_CHECK(pos == 0);
AKSL_CHECK_OK(aksl_fclose(fp));
AKSL_CHECK(unlink(path) == 0);
return 0;
}
static int test_seeko_and_tello(void)
{
char path[AKSL_TMP_MAX];
FILE *fp = NULL;
off_t pos = -1;
AKSL_CHECK(open_with_text("0123456789", path, sizeof(path), &fp) == 0);
AKSL_CHECK_OK(aksl_fseeko(fp, 6, SEEK_SET));
AKSL_CHECK_OK(aksl_ftello(fp, &pos));
AKSL_CHECK(pos == 6);
AKSL_CHECK_OK(aksl_fclose(fp));
AKSL_CHECK(unlink(path) == 0);
return 0;
}
static int test_getpos_and_setpos(void)
{
char path[AKSL_TMP_MAX];
FILE *fp = NULL;
fpos_t saved;
int c = 0;
AKSL_CHECK(open_with_text("abcdef", path, sizeof(path), &fp) == 0);
AKSL_CHECK_OK(aksl_fseek(fp, 2, SEEK_SET));
AKSL_CHECK_OK(aksl_fgetpos(fp, &saved));
AKSL_CHECK_OK(aksl_fgetc(fp, &c));
AKSL_CHECK(c == 'c');
AKSL_CHECK_OK(aksl_fsetpos(fp, &saved));
AKSL_CHECK_OK(aksl_fgetc(fp, &c));
AKSL_CHECK(c == 'c');
AKSL_CHECK_OK(aksl_fclose(fp));
AKSL_CHECK(unlink(path) == 0);
return 0;
}
static int test_positioning_rejects_null(void)
{
long pos = 0;
off_t opos = 0;
fpos_t fpos;
AKSL_CHECK_STATUS(aksl_fseek(NULL, 0, SEEK_SET), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_ftell(NULL, &pos), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_ftell(stdin, NULL), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_rewind(NULL), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_fseeko(NULL, 0, SEEK_SET), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_ftello(NULL, &opos), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_ftello(stdin, NULL), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_fgetpos(NULL, &fpos), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_fgetpos(stdin, NULL), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_fsetpos(NULL, &fpos), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_fsetpos(stdin, NULL), AKERR_NULLPOINTER);
return 0;
}
/* Seeking a pipe is ESPIPE, which is the failure rewind(3) would have hidden. */
static int test_seek_on_a_pipe_reports_espipe(void)
{
FILE *fp = popen("echo hello", "r");
long pos = 0;
AKSL_CHECK(fp != NULL);
AKSL_CHECK_STATUS(aksl_fseek(fp, 0, SEEK_SET), ESPIPE);
AKSL_CHECK_STATUS(aksl_ftell(fp, &pos), ESPIPE);
AKSL_CHECK_STATUS(aksl_rewind(fp), ESPIPE);
pclose(fp);
return 0;
}
/* ---------------------------------------------------------------------- */
/* Character and line I/O */
/* ---------------------------------------------------------------------- */
/*
* fgetc(3) returns EOF for both the end of the file and a read error. Split
* apart, a read loop needs no ferror/feof dance at the bottom of it.
*/
static int test_fgetc_separates_eof_from_error(void)
{
char path[AKSL_TMP_MAX];
FILE *fp = NULL;
int c = 0;
int seen = 0;
AKSL_CHECK(open_with_text("ab", path, sizeof(path), &fp) == 0);
AKSL_CHECK_OK(aksl_fgetc(fp, &c));
AKSL_CHECK(c == 'a');
seen++;
AKSL_CHECK_OK(aksl_fgetc(fp, &c));
AKSL_CHECK(c == 'b');
seen++;
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_fgetc(fp, &c), AKERR_EOF, "end of stream");
AKSL_CHECK(seen == 2);
AKSL_CHECK_OK(aksl_fclose(fp));
AKSL_CHECK(unlink(path) == 0);
/* A stream with no read permission is the error half of the same sentinel. */
AKSL_CHECK(aksl_temp_file(path, sizeof(path)) == 0);
AKSL_CHECK_OK(aksl_fopen(path, "w", &fp));
AKSL_CHECK_STATUS(aksl_fgetc(fp, &c), EBADF);
AKSL_CHECK_OK(aksl_fclose(fp));
AKSL_CHECK(unlink(path) == 0);
AKSL_CHECK_STATUS(aksl_fgetc(NULL, &c), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_fgetc(stdin, NULL), AKERR_NULLPOINTER);
return 0;
}
static int test_fputc_and_ungetc(void)
{
char path[AKSL_TMP_MAX];
FILE *fp = NULL;
int c = 0;
AKSL_CHECK(aksl_temp_file(path, sizeof(path)) == 0);
AKSL_CHECK_OK(aksl_fopen(path, "w", &fp));
AKSL_CHECK_OK(aksl_fputc('x', fp));
AKSL_CHECK_OK(aksl_fputc('y', fp));
AKSL_CHECK_OK(aksl_fclose(fp));
AKSL_CHECK_OK(aksl_fopen(path, "r", &fp));
AKSL_CHECK_OK(aksl_fgetc(fp, &c));
AKSL_CHECK(c == 'x');
/* Put it back and read it again. */
AKSL_CHECK_OK(aksl_ungetc(c, fp));
AKSL_CHECK_OK(aksl_fgetc(fp, &c));
AKSL_CHECK(c == 'x');
AKSL_CHECK_OK(aksl_fclose(fp));
AKSL_CHECK(unlink(path) == 0);
AKSL_CHECK_STATUS(aksl_fputc('x', NULL), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_ungetc('x', NULL), AKERR_NULLPOINTER);
return 0;
}
static int test_fgets_and_fputs(void)
{
char path[AKSL_TMP_MAX];
char line[64];
FILE *fp = NULL;
size_t len = 0;
AKSL_CHECK(aksl_temp_file(path, sizeof(path)) == 0);
AKSL_CHECK_OK(aksl_fopen(path, "w", &fp));
AKSL_CHECK_OK(aksl_fputs("first\n", fp));
AKSL_CHECK_OK(aksl_fputs("second\n", fp));
AKSL_CHECK_OK(aksl_fclose(fp));
AKSL_CHECK_OK(aksl_fopen(path, "r", &fp));
AKSL_CHECK_OK(aksl_fgets(line, sizeof(line), fp, &len));
AKSL_CHECK(strcmp(line, "first\n") == 0);
AKSL_CHECK(len == 6);
AKSL_CHECK_OK(aksl_fgets(line, sizeof(line), fp, &len));
AKSL_CHECK(strcmp(line, "second\n") == 0);
/* The end of the input is AKERR_EOF, which is how the loop terminates. */
AKSL_CHECK_STATUS(aksl_fgets(line, sizeof(line), fp, &len), AKERR_EOF);
AKSL_CHECK(len == 0);
AKSL_CHECK_OK(aksl_fclose(fp));
AKSL_CHECK(unlink(path) == 0);
return 0;
}
/*
* A line longer than the buffer is a short read, not an error -- that is what
* fgets does and what a caller reading fixed chunks wants. The reported length
* is how the caller notices: a full buffer with no newline is exactly this case.
*/
static int test_fgets_reports_a_long_line_as_a_short_read(void)
{
char path[AKSL_TMP_MAX];
char line[4];
FILE *fp = NULL;
size_t len = 0;
AKSL_CHECK(open_with_text("abcdefgh\n", path, sizeof(path), &fp) == 0);
AKSL_CHECK_OK(aksl_fgets(line, sizeof(line), fp, &len));
AKSL_CHECK(len == 3);
AKSL_CHECK(strcmp(line, "abc") == 0);
AKSL_CHECK(line[len - 1] != '\n');
/* The rest of the line is still there. */
AKSL_CHECK_OK(aksl_fgets(line, sizeof(line), fp, &len));
AKSL_CHECK(strcmp(line, "def") == 0);
AKSL_CHECK_OK(aksl_fclose(fp));
AKSL_CHECK(unlink(path) == 0);
AKSL_CHECK_STATUS(aksl_fgets(NULL, 4, stdin, &len), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_fgets(line, 4, NULL, &len), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_fgets(line, 4, stdin, NULL), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_fgets(line, 0, stdin, &len), AKERR_VALUE);
return 0;
}
/*
* getline grows the buffer, so the caller starts with NULL/0 and frees once at
* the end -- not once per line. The reported length is what distinguishes an
* embedded NUL from the end of the line; strlen cannot.
*/
static int test_getline_grows_its_buffer(void)
{
char path[AKSL_TMP_MAX];
char *line = NULL;
size_t cap = 0;
size_t len = 0;
FILE *fp = NULL;
int lines = 0;
AKSL_CHECK(open_with_text("short\na much longer line than the first one\n",
path, sizeof(path), &fp) == 0);
while ( 1 ) {
akerr_ErrorContext *raised = aksl_getline(&line, &cap, fp, &len);
int status = aksl_take(raised);
if ( status == AKERR_EOF ) {
break;
}
AKSL_CHECK(status == 0);
AKSL_CHECK(len > 0);
AKSL_CHECK(line != NULL);
lines++;
}
AKSL_CHECK(lines == 2);
/* One buffer for the whole file, grown as needed. */
AKSL_CHECK(cap >= 42);
AKSL_CHECK_OK(aksl_freep((void **)&line));
AKSL_CHECK_OK(aksl_fclose(fp));
AKSL_CHECK(unlink(path) == 0);
return 0;
}
static int test_getdelim_splits_on_any_byte(void)
{
char path[AKSL_TMP_MAX];
char *field = NULL;
size_t cap = 0;
size_t len = 0;
FILE *fp = NULL;
int fields = 0;
AKSL_CHECK(open_with_text("a:b:c", path, sizeof(path), &fp) == 0);
while ( 1 ) {
akerr_ErrorContext *raised = aksl_getdelim(&field, &cap, ':', fp, &len);
int status = aksl_take(raised);
if ( status == AKERR_EOF ) {
break;
}
AKSL_CHECK(status == 0);
fields++;
}
AKSL_CHECK(fields == 3);
AKSL_CHECK_OK(aksl_freep((void **)&field));
AKSL_CHECK_OK(aksl_fclose(fp));
AKSL_CHECK(unlink(path) == 0);
AKSL_CHECK_STATUS(aksl_getline(NULL, &cap, stdin, &len), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_getline(&field, NULL, stdin, &len), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_getline(&field, &cap, NULL, &len), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_getline(&field, &cap, stdin, NULL), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_getdelim(NULL, &cap, ':', stdin, &len), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_getdelim(&field, &cap, ':', stdin, NULL), AKERR_NULLPOINTER);
return 0;
}
/* ---------------------------------------------------------------------- */
/* Stream state */
/* ---------------------------------------------------------------------- */
static int test_stream_state(void)
{
char path[AKSL_TMP_MAX];
FILE *fp = NULL;
int flag = 99;
int fd = -1;
int c = 0;
AKSL_CHECK(open_with_text("a", path, sizeof(path), &fp) == 0);
AKSL_CHECK_OK(aksl_feof(fp, &flag));
AKSL_CHECK(flag == 0);
AKSL_CHECK_OK(aksl_ferror(fp, &flag));
AKSL_CHECK(flag == 0);
AKSL_CHECK_OK(aksl_fileno(fp, &fd));
AKSL_CHECK(fd >= 0);
/* Read past the end to set the EOF indicator, then clear it. */
AKSL_CHECK_OK(aksl_fgetc(fp, &c));
AKSL_CHECK_STATUS(aksl_fgetc(fp, &c), AKERR_EOF);
AKSL_CHECK_OK(aksl_feof(fp, &flag));
AKSL_CHECK(flag != 0);
AKSL_CHECK_OK(aksl_clearerr(fp));
AKSL_CHECK_OK(aksl_feof(fp, &flag));
AKSL_CHECK(flag == 0);
AKSL_CHECK_OK(aksl_fclose(fp));
AKSL_CHECK(unlink(path) == 0);
AKSL_CHECK_STATUS(aksl_feof(NULL, &flag), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_feof(stdin, NULL), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_ferror(NULL, &flag), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_ferror(stdin, NULL), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_clearerr(NULL), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_fileno(NULL, &fd), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_fileno(stdin, NULL), AKERR_NULLPOINTER);
return 0;
}
/* ---------------------------------------------------------------------- */
/* Flushing, buffering and opening */
/* ---------------------------------------------------------------------- */
static int test_fflush_and_setvbuf(void)
{
char path[AKSL_TMP_MAX];
char buffer[BUFSIZ];
FILE *fp = NULL;
AKSL_CHECK(aksl_temp_file(path, sizeof(path)) == 0);
AKSL_CHECK_OK(aksl_fopen(path, "w", &fp));
AKSL_CHECK_OK(aksl_setvbuf(fp, buffer, _IOFBF, sizeof(buffer)));
AKSL_CHECK_OK(aksl_fputs("buffered", fp));
AKSL_CHECK_OK(aksl_fflush(fp));
AKSL_CHECK_OK(aksl_fclose(fp));
AKSL_CHECK(unlink(path) == 0);
/* NULL means "every output stream" and is not an error, unlike everywhere else. */
AKSL_CHECK_OK(aksl_fflush(NULL));
AKSL_CHECK_STATUS(aksl_setvbuf(NULL, NULL, _IONBF, 0), AKERR_NULLPOINTER);
return 0;
}
static int test_tmpfile_fdopen_and_freopen(void)
{
char path[AKSL_TMP_MAX];
FILE *fp = NULL;
FILE *reopened = NULL;
int fd = -1;
size_t moved = 0;
/* tmpfile: no name, gone when it is closed. */
AKSL_CHECK_OK(aksl_tmpfile(&fp));
AKSL_CHECK(fp != NULL);
AKSL_CHECK_OK(aksl_fwrite("x", 1, 1, fp, &moved));
AKSL_CHECK_OK(aksl_fclose(fp));
/* fdopen: wrap a descriptor this test owns. */
AKSL_CHECK(aksl_temp_file(path, sizeof(path)) == 0);
fd = open(path, O_RDONLY);
AKSL_CHECK(fd >= 0);
AKSL_CHECK_OK(aksl_fdopen(fd, "r", &fp));
AKSL_CHECK_OK(aksl_fclose(fp)); /* closes fd too */
/* freopen: point an existing stream at a different file. */
AKSL_CHECK_OK(aksl_fopen(path, "r", &fp));
AKSL_CHECK_OK(aksl_freopen(path, "r", fp, &reopened));
AKSL_CHECK(reopened != NULL);
AKSL_CHECK_OK(aksl_fclose(reopened));
AKSL_CHECK(unlink(path) == 0);
AKSL_CHECK_STATUS(aksl_tmpfile(NULL), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_fdopen(-1, "r", &fp), AKERR_VALUE);
AKSL_CHECK_STATUS(aksl_fdopen(0, NULL, &fp), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_fdopen(0, "r", NULL), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_freopen("/tmp", "r", NULL, &fp), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_freopen("/tmp", NULL, stdin, &fp), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_freopen("/tmp", "r", stdin, NULL), AKERR_NULLPOINTER);
return 0;
}
/* ---------------------------------------------------------------------- */
/* Formatted input */
/* ---------------------------------------------------------------------- */
/*
* The reason the scanf wrappers take an expected count: scanf(3) returns how
* many conversions succeeded, and comparing that against the number written in
* the format string is a check every caller has to do by hand and eventually
* forgets. Forgetting leaves the unassigned arguments holding whatever they
* held before.
*/
static int test_sscanf_enforces_the_expected_count(void)
{
int a = 0;
int b = 0;
int assigned = 0;
AKSL_CHECK_OK(aksl_sscanf("12 34", "%d %d", 2, &assigned, &a, &b));
AKSL_CHECK(assigned == 2);
AKSL_CHECK(a == 12 && b == 34);
/* One conversion short: an error rather than a stale second variable. */
a = 0;
b = 999;
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_sscanf("12 xy", "%d %d", 2, &assigned, &a, &b),
AKERR_VALUE, "1 of 2 conversions");
AKSL_CHECK(assigned == 1);
AKSL_CHECK(b == 999);
/* expected 0 opts out and hands the count back for the caller to judge. */
AKSL_CHECK_OK(aksl_sscanf("12 xy", "%d %d", 0, &assigned, &a, &b));
AKSL_CHECK(assigned == 1);
/* Nothing matched at all. */
AKSL_CHECK_STATUS(aksl_sscanf("", "%d", 1, &assigned, &a), AKERR_VALUE);
AKSL_CHECK_STATUS(aksl_sscanf(NULL, "%d", 1, &assigned, &a), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_sscanf("1", NULL, 1, &assigned), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_sscanf("1", "%d", 1, NULL, &a), AKERR_NULLPOINTER);
return 0;
}
static int test_fscanf_reads_from_a_stream(void)
{
char path[AKSL_TMP_MAX];
FILE *fp = NULL;
int a = 0;
int b = 0;
int assigned = 0;
AKSL_CHECK(open_with_text("7 8\n", path, sizeof(path), &fp) == 0);
AKSL_CHECK_OK(aksl_fscanf(fp, "%d %d", 2, &assigned, &a, &b));
AKSL_CHECK(assigned == 2);
AKSL_CHECK(a == 7 && b == 8);
/* Nothing left: the end of the stream, not a conversion failure. */
AKSL_CHECK_STATUS(aksl_fscanf(fp, "%d", 1, &assigned, &a), AKERR_EOF);
AKSL_CHECK_OK(aksl_fclose(fp));
AKSL_CHECK(unlink(path) == 0);
AKSL_CHECK_STATUS(aksl_fscanf(NULL, "%d", 1, &assigned, &a), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_fscanf(stdin, NULL, 1, &assigned), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_fscanf(stdin, "%d", 1, NULL, &a), AKERR_NULLPOINTER);
return 0;
}
/*
* aksl_scanf reads stdin, so stdin is pointed at a temp file for the duration of
* the call and restored through a dup of the original descriptor -- the same
* dance tests/test_format.c does for aksl_printf and stdout, and for the same
* reason: nothing between the freopen and the dup2 may return early.
*/
static int test_scanf_reads_stdin(void)
{
char path[AKSL_TMP_MAX];
FILE *fp = NULL;
akerr_ErrorContext *err = NULL;
int a = 0;
int b = 0;
int assigned = 0;
int saved = -1;
int restored = -1;
AKSL_CHECK(aksl_temp_file(path, sizeof(path)) == 0);
fp = fopen(path, "w");
AKSL_CHECK(fp != NULL);
AKSL_CHECK(fputs("11 22\n", fp) != EOF);
AKSL_CHECK(fclose(fp) == 0);
saved = dup(fileno(stdin));
AKSL_CHECK(saved >= 0);
if ( freopen(path, "r", stdin) == NULL ) {
close(saved);
AKSL_CHECK(0);
}
err = aksl_scanf("%d %d", 2, &assigned, &a, &b);
restored = dup2(saved, fileno(stdin));
close(saved);
clearerr(stdin);
AKSL_CHECK(restored >= 0);
AKSL_CHECK(aksl_take(err) == 0);
AKSL_CHECK(assigned == 2);
AKSL_CHECK(a == 11 && b == 22);
AKSL_CHECK(unlink(path) == 0);
AKSL_CHECK_STATUS(aksl_scanf(NULL, 1, &assigned), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_scanf("%d", 1, NULL, &a), AKERR_NULLPOINTER);
return 0;
}
/* The va_list form, used the way a consumer building its own wrapper would. */
static akerr_ErrorContext AKERR_NOIGNORE *consumer_scan(const char *fmt, int expected,
int *assigned, ...)
{
va_list args;
akerr_ErrorContext *raised = NULL;
va_start(args, assigned);
raised = aksl_vscanf(fmt, expected, assigned, args);
va_end(args);
return raised;
}
static int test_vscanf_reads_stdin(void)
{
char path[AKSL_TMP_MAX];
FILE *fp = NULL;
akerr_ErrorContext *err = NULL;
int a = 0;
int assigned = 0;
int saved = -1;
int restored = -1;
AKSL_CHECK(aksl_temp_file(path, sizeof(path)) == 0);
fp = fopen(path, "w");
AKSL_CHECK(fp != NULL);
AKSL_CHECK(fputs("33\n", fp) != EOF);
AKSL_CHECK(fclose(fp) == 0);
saved = dup(fileno(stdin));
AKSL_CHECK(saved >= 0);
if ( freopen(path, "r", stdin) == NULL ) {
close(saved);
AKSL_CHECK(0);
}
err = consumer_scan("%d", 1, &assigned, &a);
restored = dup2(saved, fileno(stdin));
close(saved);
clearerr(stdin);
AKSL_CHECK(restored >= 0);
AKSL_CHECK(aksl_take(err) == 0);
AKSL_CHECK(assigned == 1);
AKSL_CHECK(a == 33);
AKSL_CHECK(unlink(path) == 0);
return 0;
}
/* ---------------------------------------------------------------------- */
/* Files */
/* ---------------------------------------------------------------------- */
static int test_remove_and_rename(void)
{
char path[AKSL_TMP_MAX];
char moved[AKSL_TMP_MAX + 8];
AKSL_CHECK(aksl_temp_file(path, sizeof(path)) == 0);
AKSL_CHECK((size_t)snprintf(moved, sizeof(moved), "%s.moved", path) < sizeof(moved));
AKSL_CHECK_OK(aksl_rename(path, moved));
AKSL_CHECK(access(path, F_OK) != 0);
AKSL_CHECK(access(moved, F_OK) == 0);
AKSL_CHECK_OK(aksl_remove(moved));
AKSL_CHECK(access(moved, F_OK) != 0);
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_remove("/nonexistent/aksl/file"),
ENOENT, "/nonexistent/aksl/file");
AKSL_CHECK_STATUS(aksl_remove(NULL), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_rename(NULL, "b"), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_rename("a", NULL), AKERR_NULLPOINTER);
return 0;
}
/*
* mkstemp and mkdtemp rewrite the template in place, so a template that is not
* a writable buffer of the right shape is a crash waiting to happen. The shape
* is checked here rather than left to the kernel.
*/
static int test_mkstemp_and_mkdtemp(void)
{
char file_template[] = "/tmp/aksl_streamio_XXXXXX";
char dir_template[] = "/tmp/aksl_streamio_dir_XXXXXX";
char bad[] = "/tmp/aksl_no_placeholder";
int fd = -1;
AKSL_CHECK_OK(aksl_mkstemp(file_template, &fd));
AKSL_CHECK(fd >= 0);
AKSL_CHECK(strcmp(file_template, "/tmp/aksl_streamio_XXXXXX") != 0);
AKSL_CHECK(close(fd) == 0);
AKSL_CHECK_OK(aksl_remove(file_template));
AKSL_CHECK_OK(aksl_mkdtemp(dir_template));
AKSL_CHECK(access(dir_template, F_OK) == 0);
AKSL_CHECK(rmdir(dir_template) == 0);
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_mkstemp(bad, &fd), AKERR_VALUE, "six literal X");
AKSL_CHECK(fd == -1);
AKSL_CHECK_STATUS(aksl_mkdtemp(bad), AKERR_VALUE);
AKSL_CHECK_STATUS(aksl_mkstemp(NULL, &fd), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_mkstemp(file_template, NULL), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_mkdtemp(NULL), AKERR_NULLPOINTER);
return 0;
}
int main(void)
{
int failures = 0;
akerr_init();
AKSL_RUN(failures, test_seek_and_tell);
AKSL_RUN(failures, test_seeko_and_tello);
AKSL_RUN(failures, test_getpos_and_setpos);
AKSL_RUN(failures, test_positioning_rejects_null);
AKSL_RUN(failures, test_seek_on_a_pipe_reports_espipe);
AKSL_RUN(failures, test_fgetc_separates_eof_from_error);
AKSL_RUN(failures, test_fputc_and_ungetc);
AKSL_RUN(failures, test_fgets_and_fputs);
AKSL_RUN(failures, test_fgets_reports_a_long_line_as_a_short_read);
AKSL_RUN(failures, test_getline_grows_its_buffer);
AKSL_RUN(failures, test_getdelim_splits_on_any_byte);
AKSL_RUN(failures, test_stream_state);
AKSL_RUN(failures, test_fflush_and_setvbuf);
AKSL_RUN(failures, test_tmpfile_fdopen_and_freopen);
AKSL_RUN(failures, test_sscanf_enforces_the_expected_count);
AKSL_RUN(failures, test_fscanf_reads_from_a_stream);
AKSL_RUN(failures, test_scanf_reads_stdin);
AKSL_RUN(failures, test_vscanf_reads_stdin);
AKSL_RUN(failures, test_remove_and_rename);
AKSL_RUN(failures, test_mkstemp_and_mkdtemp);
AKSL_REPORT(failures);
}

148
tests/test_strhash.c Normal file
View File

@@ -0,0 +1,148 @@
/*
* aksl_strhash_djb2 -- TODO.md section 1.6.
*
* The expected values are the canonical djb2 ones: h = 5381, then
* h = h * 33 + byte for each of len bytes, truncated to 32 bits. They were
* computed independently of this implementation.
*
* Most vectors here are 7-bit ASCII, where signed and unsigned char agree and
* the test therefore says nothing either way about byte signedness. The high-bit
* vector is the one that pins TODO.md 2.2.6 down.
*/
#include "aksl_capture.h"
static int test_empty_string_is_the_djb2_seed(void)
{
uint32_t h = 0;
AKSL_CHECK_OK(aksl_strhash_djb2("", 0, &h));
AKSL_CHECK(h == 5381);
return 0;
}
/* len drives the loop, so a zero length ignores the contents entirely. */
static int test_zero_length_ignores_the_buffer(void)
{
char buf[] = "ignored";
uint32_t h = 0;
AKSL_CHECK_OK(aksl_strhash_djb2(buf, 0, &h));
AKSL_CHECK(h == 5381);
return 0;
}
static int test_known_answer_vectors(void)
{
char hello[] = "hello";
char libname[] = "libakstdlib";
uint32_t h = 0;
AKSL_CHECK_OK(aksl_strhash_djb2(hello, 5, &h));
AKSL_CHECK(h == 261238937u);
AKSL_CHECK_OK(aksl_strhash_djb2(libname, 11, &h));
AKSL_CHECK(h == 884285482u);
return 0;
}
/*
* TODO.md 2.2.6, pinned. The cursor is an unsigned char * now, so bytes at or
* above 0x80 contribute their unsigned value. Iterating a plain char * on x86 or
* ARM Linux made them negative, giving 5859874 here instead of 5868578 -- a hash
* that disagreed with canonical djb2 and, worse, disagreed with itself across
* platforms depending on whether char happened to be signed.
*
* Benign for the 7-bit identifiers akbasic hashes; quietly wrong for the first
* caller to key a table on a filename or a UTF-8 string literal.
*/
static int test_high_bit_bytes_are_unsigned(void)
{
char buf[2] = { (char)0xff, (char)0xfe };
uint32_t h = 0;
AKSL_CHECK_OK(aksl_strhash_djb2(buf, sizeof(buf), &h));
AKSL_CHECK(h == 5868578u);
/* The sign-extended answer, spelled out so a regression names itself. */
AKSL_CHECK(h != 5859874u);
return 0;
}
/* The NUL-terminated convenience form (TODO.md 3.6) agrees with the length one. */
static int test_str_form_matches_the_length_form(void)
{
const char *s = "libakstdlib";
uint32_t from_str = 0;
uint32_t from_len = 0;
AKSL_CHECK_OK(aksl_strhash_djb2_str(s, &from_str));
AKSL_CHECK_OK(aksl_strhash_djb2(s, strlen(s), &from_len));
AKSL_CHECK(from_str == from_len);
AKSL_CHECK(from_str == 884285482u);
/* The empty string is the seed, not an error. */
AKSL_CHECK_OK(aksl_strhash_djb2_str("", &from_str));
AKSL_CHECK(from_str == 5381);
AKSL_CHECK_STATUS(aksl_strhash_djb2_str(NULL, &from_str), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strhash_djb2_str("x", NULL), AKERR_NULLPOINTER);
return 0;
}
/* The function is length-driven, not NUL-driven: an embedded NUL is hashed. */
static int test_embedded_nul_is_hashed(void)
{
char buf[3] = { 'a', '\0', 'b' };
uint32_t whole = 0;
uint32_t prefix = 0;
AKSL_CHECK_OK(aksl_strhash_djb2(buf, sizeof(buf), &whole));
AKSL_CHECK(whole == 193482728u);
/* Stopping at the NUL would give the one-byte hash instead. */
AKSL_CHECK_OK(aksl_strhash_djb2(buf, 1, &prefix));
AKSL_CHECK(prefix != whole);
return 0;
}
static int test_hash_is_stable_across_calls(void)
{
char buf[] = "repeatable";
uint32_t first = 0;
uint32_t second = 0;
AKSL_CHECK_OK(aksl_strhash_djb2(buf, sizeof(buf) - 1, &first));
AKSL_CHECK_OK(aksl_strhash_djb2(buf, sizeof(buf) - 1, &second));
AKSL_CHECK(first == second);
return 0;
}
static int test_rejects_null_arguments(void)
{
char buf[] = "x";
uint32_t h = 0;
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_strhash_djb2(NULL, 1, &h),
AKERR_NULLPOINTER, "str");
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_strhash_djb2(buf, 1, NULL),
AKERR_NULLPOINTER, "hashval");
return 0;
}
int main(void)
{
int failures = 0;
akerr_init();
AKSL_RUN(failures, test_empty_string_is_the_djb2_seed);
AKSL_RUN(failures, test_zero_length_ignores_the_buffer);
AKSL_RUN(failures, test_known_answer_vectors);
AKSL_RUN(failures, test_high_bit_bytes_are_unsigned);
AKSL_RUN(failures, test_str_form_matches_the_length_form);
AKSL_RUN(failures, test_embedded_nul_is_hashed);
AKSL_RUN(failures, test_hash_is_stable_across_calls);
AKSL_RUN(failures, test_rejects_null_arguments);
AKSL_REPORT(failures);
}

448
tests/test_string.c Normal file
View File

@@ -0,0 +1,448 @@
/*
* String wrappers -- src/string.c, TODO.md section 3.1.
*
* The two contracts worth testing hardest are the ones that differ from libc:
* every copying function takes the destination size and treats truncation as an
* error that writes nothing, and every searching function treats "not found" as
* a successful answer of NULL.
*/
#include "aksl_capture.h"
#include <errno.h>
/* ---------------------------------------------------------------------- */
/* Length */
/* ---------------------------------------------------------------------- */
static int test_strlen_and_strnlen(void)
{
size_t n = 99;
AKSL_CHECK_OK(aksl_strlen("hello", &n));
AKSL_CHECK(n == 5);
AKSL_CHECK_OK(aksl_strlen("", &n));
AKSL_CHECK(n == 0);
/* strnlen stops at maxlen whether or not it found a terminator. */
AKSL_CHECK_OK(aksl_strnlen("hello", 3, &n));
AKSL_CHECK(n == 3);
AKSL_CHECK_OK(aksl_strnlen("hello", 99, &n));
AKSL_CHECK(n == 5);
AKSL_CHECK_STATUS(aksl_strlen(NULL, &n), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strlen("x", NULL), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strnlen(NULL, 1, &n), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strnlen("x", 1, NULL), AKERR_NULLPOINTER);
return 0;
}
/* ---------------------------------------------------------------------- */
/* Copying */
/* ---------------------------------------------------------------------- */
static int test_strcpy_copies_within_the_buffer(void)
{
char buf[8];
AKSL_CHECK_OK(aksl_strcpy(buf, sizeof(buf), "abc"));
AKSL_CHECK(strcmp(buf, "abc") == 0);
/* Exactly filling the buffer, terminator included, is not truncation. */
AKSL_CHECK_OK(aksl_strcpy(buf, sizeof(buf), "1234567"));
AKSL_CHECK(strcmp(buf, "1234567") == 0);
return 0;
}
/*
* The whole point of taking dstsize. akbasic writes this check by hand at ten
* sites: a length test, then strncpy, then an explicit NUL.
*/
static int test_strcpy_truncation_is_an_error_and_writes_nothing(void)
{
char buf[8] = "previous";
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_strcpy(buf, sizeof(buf), "12345678"),
AKERR_OUTOFBOUNDS, "destination holds");
/*
* Empty, not a truncated prefix. A caller who ignores the status gets
* nothing, which is far easier to notice than a plausible-looking "1234567".
*/
AKSL_CHECK(buf[0] == '\0');
return 0;
}
/*
* strncpy(3) leaves the destination unterminated when the source is at least n
* bytes, and NUL-pads the whole remainder when it is shorter. This does neither.
*/
static int test_strncpy_always_terminates_and_never_pads(void)
{
char buf[16];
memset(buf, 'Z', sizeof(buf));
AKSL_CHECK_OK(aksl_strncpy(buf, sizeof(buf), "abcdef", 3));
AKSL_CHECK(strcmp(buf, "abc") == 0);
/* Not padded: everything past the terminator is untouched. */
AKSL_CHECK(buf[4] == 'Z');
/* n larger than the source just copies the source. */
AKSL_CHECK_OK(aksl_strncpy(buf, sizeof(buf), "abc", 99));
AKSL_CHECK(strcmp(buf, "abc") == 0);
/* n bytes that do not fit the destination is still an error. */
AKSL_CHECK_STATUS(aksl_strncpy(buf, 4, "abcdef", 6), AKERR_OUTOFBOUNDS);
AKSL_CHECK(buf[0] == '\0');
return 0;
}
static int test_strcat_appends_within_the_buffer(void)
{
char buf[16] = "ab";
AKSL_CHECK_OK(aksl_strcat(buf, sizeof(buf), "cd"));
AKSL_CHECK(strcmp(buf, "abcd") == 0);
AKSL_CHECK_OK(aksl_strcat(buf, sizeof(buf), ""));
AKSL_CHECK(strcmp(buf, "abcd") == 0);
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_strcat(buf, sizeof(buf), "0123456789ab"),
AKERR_OUTOFBOUNDS, "in use plus");
/* The existing contents survive a refused append. */
AKSL_CHECK(strcmp(buf, "abcd") == 0);
return 0;
}
/* An unterminated destination is refused rather than walked off the end of. */
static int test_strcat_refuses_an_unterminated_destination(void)
{
char buf[4];
memset(buf, 'x', sizeof(buf));
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_strcat(buf, sizeof(buf), "y"),
AKERR_VALUE, "not terminated");
return 0;
}
static int test_strncat_appends_at_most_n(void)
{
char buf[16] = "ab";
AKSL_CHECK_OK(aksl_strncat(buf, sizeof(buf), "cdef", 2));
AKSL_CHECK(strcmp(buf, "abcd") == 0);
AKSL_CHECK_STATUS(aksl_strncat(buf, 6, "xyz", 3), AKERR_OUTOFBOUNDS);
AKSL_CHECK(strcmp(buf, "abcd") == 0);
return 0;
}
static int test_copying_rejects_null_and_zero_size(void)
{
char buf[8] = "";
AKSL_CHECK_STATUS(aksl_strcpy(NULL, 8, "x"), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strcpy(buf, sizeof(buf), NULL), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strcpy(buf, 0, "x"), AKERR_VALUE);
AKSL_CHECK_STATUS(aksl_strncpy(NULL, 8, "x", 1), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strncpy(buf, sizeof(buf), NULL, 1), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strncpy(buf, 0, "x", 1), AKERR_VALUE);
AKSL_CHECK_STATUS(aksl_strcat(NULL, 8, "x"), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strcat(buf, sizeof(buf), NULL), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strcat(buf, 0, "x"), AKERR_VALUE);
AKSL_CHECK_STATUS(aksl_strncat(NULL, 8, "x", 1), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strncat(buf, sizeof(buf), NULL, 1), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strncat(buf, 0, "x", 1), AKERR_VALUE);
return 0;
}
/* ---------------------------------------------------------------------- */
/* Duplication */
/* ---------------------------------------------------------------------- */
static int test_strdup_and_strndup(void)
{
char *copy = NULL;
AKSL_CHECK_OK(aksl_strdup("duplicate me", &copy));
AKSL_CHECK(copy != NULL);
AKSL_CHECK(strcmp(copy, "duplicate me") == 0);
AKSL_CHECK_OK(aksl_freep((void **)&copy));
AKSL_CHECK(copy == NULL);
AKSL_CHECK_OK(aksl_strndup("duplicate me", 9, &copy));
AKSL_CHECK(strcmp(copy, "duplicate") == 0);
AKSL_CHECK_OK(aksl_freep((void **)&copy));
/* n past the end of the string just copies the string. */
AKSL_CHECK_OK(aksl_strndup("ab", 99, &copy));
AKSL_CHECK(strcmp(copy, "ab") == 0);
AKSL_CHECK_OK(aksl_freep((void **)&copy));
AKSL_CHECK_STATUS(aksl_strdup(NULL, &copy), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strdup("x", NULL), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strndup(NULL, 1, &copy), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strndup("x", 1, NULL), AKERR_NULLPOINTER);
return 0;
}
/* ---------------------------------------------------------------------- */
/* Comparison */
/* ---------------------------------------------------------------------- */
static int test_comparisons(void)
{
int r = 99;
AKSL_CHECK_OK(aksl_strcmp("abc", "abc", &r));
AKSL_CHECK(r == 0);
AKSL_CHECK_OK(aksl_strcmp("abc", "abd", &r));
AKSL_CHECK(r < 0);
AKSL_CHECK_OK(aksl_strcmp("abd", "abc", &r));
AKSL_CHECK(r > 0);
AKSL_CHECK_OK(aksl_strncmp("abcXX", "abcYY", 3, &r));
AKSL_CHECK(r == 0);
AKSL_CHECK_OK(aksl_strncmp("abcXX", "abcYY", 4, &r));
AKSL_CHECK(r != 0);
/* The three case-folding sites akbasic writes out by hand. */
AKSL_CHECK_OK(aksl_strcasecmp("PRINT", "print", &r));
AKSL_CHECK(r == 0);
AKSL_CHECK_OK(aksl_strcasecmp("PRINT", "input", &r));
AKSL_CHECK(r != 0);
AKSL_CHECK_OK(aksl_strncasecmp("PRINTXX", "printYY", 5, &r));
AKSL_CHECK(r == 0);
AKSL_CHECK_OK(aksl_strcoll("abc", "abc", &r));
AKSL_CHECK(r == 0);
AKSL_CHECK_STATUS(aksl_strcmp(NULL, "b", &r), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strcmp("a", NULL, &r), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strcmp("a", "b", NULL), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strncmp(NULL, "b", 1, &r), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strncmp("a", NULL, 1, &r), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strncmp("a", "b", 1, NULL), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strcasecmp(NULL, "b", &r), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strcasecmp("a", NULL, &r), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strcasecmp("a", "b", NULL), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strncasecmp(NULL, "b", 1, &r), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strncasecmp("a", NULL, 1, &r), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strncasecmp("a", "b", 1, NULL), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strcoll(NULL, "b", &r), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strcoll("a", NULL, &r), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strcoll("a", "b", NULL), AKERR_NULLPOINTER);
return 0;
}
/* ---------------------------------------------------------------------- */
/* Searching */
/* ---------------------------------------------------------------------- */
/*
* Every one of these treats absence as an answer. A library that raised on it
* would have every caller catching a non-error, and burning a pool slot to do
* so.
*/
static int test_searching_reports_absence_as_success(void)
{
const char *s = "the quick brown fox";
char *at = (char *)0x1;
size_t n = 99;
AKSL_CHECK_OK(aksl_strchr(s, 'q', &at));
AKSL_CHECK(at == s + 4);
AKSL_CHECK_OK(aksl_strchr(s, 'Z', &at));
AKSL_CHECK(at == NULL);
AKSL_CHECK_OK(aksl_strrchr(s, 'o', &at));
AKSL_CHECK(at == s + 17);
AKSL_CHECK_OK(aksl_strrchr(s, 'Z', &at));
AKSL_CHECK(at == NULL);
AKSL_CHECK_OK(aksl_strstr(s, "brown", &at));
AKSL_CHECK(at == s + 10);
AKSL_CHECK_OK(aksl_strstr(s, "purple", &at));
AKSL_CHECK(at == NULL);
AKSL_CHECK_OK(aksl_strpbrk(s, "xq", &at));
AKSL_CHECK(at == s + 4);
AKSL_CHECK_OK(aksl_strpbrk(s, "ZY", &at));
AKSL_CHECK(at == NULL);
AKSL_CHECK_OK(aksl_strspn("aaabbb", "a", &n));
AKSL_CHECK(n == 3);
AKSL_CHECK_OK(aksl_strcspn("aaabbb", "b", &n));
AKSL_CHECK(n == 3);
return 0;
}
/* The open-coded case-insensitive search, including its edge cases. */
static int test_strcasestr(void)
{
const char *s = "The Quick Brown Fox";
char *at = (char *)0x1;
AKSL_CHECK_OK(aksl_strcasestr(s, "quick", &at));
AKSL_CHECK(at == s + 4);
AKSL_CHECK_OK(aksl_strcasestr(s, "QUICK", &at));
AKSL_CHECK(at == s + 4);
AKSL_CHECK_OK(aksl_strcasestr(s, "purple", &at));
AKSL_CHECK(at == NULL);
/* An empty needle matches at the start, as strstr(3) has it. */
AKSL_CHECK_OK(aksl_strcasestr(s, "", &at));
AKSL_CHECK(at == s);
/* A needle longer than the haystack cannot match, and must not read past. */
AKSL_CHECK_OK(aksl_strcasestr("ab", "abcdef", &at));
AKSL_CHECK(at == NULL);
/* A match right at the end. */
AKSL_CHECK_OK(aksl_strcasestr(s, "fox", &at));
AKSL_CHECK(at == s + 16);
return 0;
}
static int test_searching_rejects_null_arguments(void)
{
char *at = NULL;
size_t n = 0;
AKSL_CHECK_STATUS(aksl_strchr(NULL, 'a', &at), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strchr("a", 'a', NULL), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strrchr(NULL, 'a', &at), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strrchr("a", 'a', NULL), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strstr(NULL, "a", &at), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strstr("a", NULL, &at), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strstr("a", "a", NULL), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strcasestr(NULL, "a", &at), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strcasestr("a", NULL, &at), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strcasestr("a", "a", NULL), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strpbrk(NULL, "a", &at), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strpbrk("a", NULL, &at), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strpbrk("a", "a", NULL), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strspn(NULL, "a", &n), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strspn("a", NULL, &n), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strspn("a", "a", NULL), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strcspn(NULL, "a", &n), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strcspn("a", NULL, &n), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strcspn("a", "a", NULL), AKERR_NULLPOINTER);
return 0;
}
/* ---------------------------------------------------------------------- */
/* Tokenising */
/* ---------------------------------------------------------------------- */
static int test_strtok_r_walks_the_tokens(void)
{
char input[] = "one,two,,three";
char *save = NULL;
char *tok = NULL;
int seen = 0;
AKSL_CHECK_OK(aksl_strtok_r(input, ",", &save, &tok));
AKSL_CHECK(tok != NULL && strcmp(tok, "one") == 0);
seen++;
while ( 1 ) {
AKSL_CHECK_OK(aksl_strtok_r(NULL, ",", &save, &tok));
if ( tok == NULL ) {
break;
}
seen++;
}
/* strtok_r collapses the empty field between the two commas. */
AKSL_CHECK(seen == 3);
AKSL_CHECK_STATUS(aksl_strtok_r(NULL, NULL, &save, &tok), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strtok_r(NULL, ",", NULL, &tok), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strtok_r(NULL, ",", &save, NULL), AKERR_NULLPOINTER);
return 0;
}
/* strsep keeps the empty fields, which is why it exists alongside strtok_r. */
static int test_strsep_keeps_empty_fields(void)
{
char input[] = "one,two,,three";
char *cursor = input;
char *tok = NULL;
int seen = 0;
while ( cursor != NULL ) {
AKSL_CHECK_OK(aksl_strsep(&cursor, ",", &tok));
if ( tok == NULL ) {
break;
}
seen++;
}
AKSL_CHECK(seen == 4);
AKSL_CHECK_STATUS(aksl_strsep(NULL, ",", &tok), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strsep(&cursor, NULL, &tok), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strsep(&cursor, ",", NULL), AKERR_NULLPOINTER);
return 0;
}
/* ---------------------------------------------------------------------- */
/* Status messages */
/* ---------------------------------------------------------------------- */
/*
* aksl_strerror knows this library's own statuses as well as errno values,
* which is the reason it is not a strerror_r wrapper: strerror_r could never
* name AKERR_NULLPOINTER.
*/
static int test_strerror_names_both_kinds_of_status(void)
{
char buf[128];
AKSL_CHECK_OK(aksl_strerror(ENOENT, buf, sizeof(buf)));
AKSL_CHECK(buf[0] != '\0');
AKSL_CHECK(strcmp(buf, "Unknown status 2") != 0);
AKSL_CHECK_OK(aksl_strerror(AKERR_NULLPOINTER, buf, sizeof(buf)));
AKSL_CHECK(buf[0] != '\0');
/* A status nothing has a name for is rendered as its number. */
AKSL_CHECK_OK(aksl_strerror(-98765, buf, sizeof(buf)));
AKSL_CHECK(strcmp(buf, "Unknown status -98765") == 0);
/* Too small a buffer is an error, and leaves nothing rather than a prefix. */
AKSL_CHECK_STATUS(aksl_strerror(ENOENT, buf, 2), AKERR_OUTOFBOUNDS);
AKSL_CHECK(buf[0] == '\0');
AKSL_CHECK_STATUS(aksl_strerror(ENOENT, NULL, 16), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strerror(ENOENT, buf, 0), AKERR_VALUE);
return 0;
}
int main(void)
{
int failures = 0;
akerr_init();
AKSL_RUN(failures, test_strlen_and_strnlen);
AKSL_RUN(failures, test_strcpy_copies_within_the_buffer);
AKSL_RUN(failures, test_strcpy_truncation_is_an_error_and_writes_nothing);
AKSL_RUN(failures, test_strncpy_always_terminates_and_never_pads);
AKSL_RUN(failures, test_strcat_appends_within_the_buffer);
AKSL_RUN(failures, test_strcat_refuses_an_unterminated_destination);
AKSL_RUN(failures, test_strncat_appends_at_most_n);
AKSL_RUN(failures, test_copying_rejects_null_and_zero_size);
AKSL_RUN(failures, test_strdup_and_strndup);
AKSL_RUN(failures, test_comparisons);
AKSL_RUN(failures, test_searching_reports_absence_as_success);
AKSL_RUN(failures, test_strcasestr);
AKSL_RUN(failures, test_searching_rejects_null_arguments);
AKSL_RUN(failures, test_strtok_r_walks_the_tokens);
AKSL_RUN(failures, test_strsep_keeps_empty_fields);
AKSL_RUN(failures, test_strerror_names_both_kinds_of_status);
AKSL_REPORT(failures);
}

340
tests/test_strto.c Normal file
View File

@@ -0,0 +1,340 @@
/*
* The strto* family -- TODO.md section 3.1.
*
* These are the real implementation behind the ato* wrappers and the thing
* akbasic had to hand-write for itself (its src/convert.c, ~60 lines) because
* the library would not do it. What they add over the ato* forms is a base and
* an endptr: pass a non-NULL endptr to parse a number off the front of a longer
* string and find out where it stopped, and the trailing-junk check goes away
* because the caller has taken responsibility for what follows.
*
* tests/test_convert.c covers what the two families share; this file is the part
* that is specific to having a base and an endptr.
*/
#include "aksl_capture.h"
#include <errno.h>
#include <limits.h>
/* ---------------------------------------------------------------------- */
/* Base handling */
/* ---------------------------------------------------------------------- */
static int test_base_is_honoured(void)
{
long out = 0;
AKSL_CHECK_OK(aksl_strtol("ff", NULL, 16, &out));
AKSL_CHECK(out == 255);
AKSL_CHECK_OK(aksl_strtol("1010", NULL, 2, &out));
AKSL_CHECK(out == 10);
AKSL_CHECK_OK(aksl_strtol("777", NULL, 8, &out));
AKSL_CHECK(out == 511);
AKSL_CHECK_OK(aksl_strtol("z", NULL, 36, &out));
AKSL_CHECK(out == 35);
return 0;
}
/* Base 0 auto-detects the 0x and 0 prefixes, which is the counterpart to
* tests/test_convert.c's test that aksl_atoi refuses "0x10". */
static int test_base_zero_detects_the_prefix(void)
{
long out = 0;
AKSL_CHECK_OK(aksl_strtol("0x10", NULL, 0, &out));
AKSL_CHECK(out == 16);
AKSL_CHECK_OK(aksl_strtol("010", NULL, 0, &out));
AKSL_CHECK(out == 8);
AKSL_CHECK_OK(aksl_strtol("10", NULL, 0, &out));
AKSL_CHECK(out == 10);
return 0;
}
/* A digit that does not belong to the base is where the number ends. */
static int test_digits_outside_the_base_end_the_number(void)
{
long out = 0;
char *end = NULL;
/* "9" is not an octal digit, so with a NULL endptr it is trailing junk. */
AKSL_CHECK_STATUS(aksl_strtol("129", NULL, 8, &out), AKERR_VALUE);
/* With an endptr the caller gets the partial parse and the stopping point. */
AKSL_CHECK_OK(aksl_strtol("129", &end, 8, &out));
AKSL_CHECK(out == 10); /* 012 octal */
AKSL_CHECK(end != NULL && *end == '9');
return 0;
}
/* An invalid base is EINVAL from strtol(3) rather than a silent answer. */
static int test_invalid_base_is_rejected(void)
{
long out = 99;
AKSL_CHECK_STATUS(aksl_strtol("10", NULL, 37, &out), EINVAL);
AKSL_CHECK(out == 0);
AKSL_CHECK_STATUS(aksl_strtol("10", NULL, 1, &out), EINVAL);
AKSL_CHECK(out == 0);
return 0;
}
/* ---------------------------------------------------------------------- */
/* endptr */
/* ---------------------------------------------------------------------- */
/*
* With an endptr, trailing text is the caller's business and not an error. This
* is the shape a tokenizer wants: parse a number, carry on from where it ended.
*/
static int test_endptr_reports_where_parsing_stopped(void)
{
const char *input = "42 rest of the line";
char *end = NULL;
long out = 0;
AKSL_CHECK_OK(aksl_strtol(input, &end, 10, &out));
AKSL_CHECK(out == 42);
AKSL_CHECK(end == input + 2);
AKSL_CHECK(strcmp(end, " rest of the line") == 0);
return 0;
}
/* Consuming nothing is still an error, endptr or not: there was no number. */
static int test_no_digits_is_an_error_even_with_an_endptr(void)
{
char *end = (char *)0x1;
long out = 99;
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_strtol("nope", &end, 10, &out),
AKERR_VALUE, "no digits");
AKSL_CHECK(out == 0);
return 0;
}
/*
* Every form writes through endptr, not just the two the other tests happen to
* use. Each type has its own function body, so "strtol handles endptr" says
* nothing whatsoever about strtoull.
*/
static int test_every_form_writes_the_endptr(void)
{
const char *input = "42rest";
char *end = NULL;
long lout = 0;
long long llout = 0;
unsigned long ulout = 0;
unsigned long long ullout = 0;
double dout = 0.0;
float fout = 0.0f;
long double ldout = 0.0L;
end = NULL;
AKSL_CHECK_OK(aksl_strtol(input, &end, 10, &lout));
AKSL_CHECK(lout == 42 && strcmp(end, "rest") == 0);
end = NULL;
AKSL_CHECK_OK(aksl_strtoll(input, &end, 10, &llout));
AKSL_CHECK(llout == 42 && strcmp(end, "rest") == 0);
end = NULL;
AKSL_CHECK_OK(aksl_strtoul(input, &end, 10, &ulout));
AKSL_CHECK(ulout == 42 && strcmp(end, "rest") == 0);
end = NULL;
AKSL_CHECK_OK(aksl_strtoull(input, &end, 10, &ullout));
AKSL_CHECK(ullout == 42 && strcmp(end, "rest") == 0);
end = NULL;
AKSL_CHECK_OK(aksl_strtod(input, &end, &dout));
AKSL_CHECK(dout == 42.0 && strcmp(end, "rest") == 0);
end = NULL;
AKSL_CHECK_OK(aksl_strtof(input, &end, &fout));
AKSL_CHECK(fout == 42.0f && strcmp(end, "rest") == 0);
end = NULL;
AKSL_CHECK_OK(aksl_strtold(input, &end, &ldout));
AKSL_CHECK(ldout == 42.0L && strcmp(end, "rest") == 0);
return 0;
}
/* Successive calls chained through endptr walk a whole list of numbers. */
static int test_endptr_chains_across_a_list(void)
{
const char *input = "1,2,3";
char *cursor = (char *)input;
long out = 0;
long total = 0;
int seen = 0;
while ( *cursor != '\0' ) {
AKSL_CHECK_OK(aksl_strtol(cursor, &cursor, 10, &out));
total += out;
seen += 1;
if ( *cursor == ',' ) {
cursor++;
}
}
AKSL_CHECK(seen == 3);
AKSL_CHECK(total == 6);
return 0;
}
/* ---------------------------------------------------------------------- */
/* Ranges and signs */
/* ---------------------------------------------------------------------- */
static int test_range_errors_per_type(void)
{
long lout = 99;
long long llout = 99;
unsigned long ulout = 99;
unsigned long long ullout = 99;
char buf[64];
snprintf(buf, sizeof(buf), "%lld9", (long long)LLONG_MAX);
AKSL_CHECK_STATUS(aksl_strtol(buf, NULL, 10, &lout), ERANGE);
AKSL_CHECK(lout == 0);
AKSL_CHECK_STATUS(aksl_strtoll(buf, NULL, 10, &llout), ERANGE);
AKSL_CHECK(llout == 0);
snprintf(buf, sizeof(buf), "%llu9", (unsigned long long)ULLONG_MAX);
AKSL_CHECK_STATUS(aksl_strtoul(buf, NULL, 10, &ulout), ERANGE);
AKSL_CHECK(ulout == 0);
AKSL_CHECK_STATUS(aksl_strtoull(buf, NULL, 10, &ullout), ERANGE);
AKSL_CHECK(ullout == 0);
return 0;
}
/*
* strtoul(3) and strtoull(3) accept a leading '-' and hand back the negation
* wrapped into the unsigned range, so "-1" parses as ULONG_MAX and reports
* nothing at all. That is exactly the silent failure this library exists to
* surface, so the sign is refused before the call.
*/
static int test_unsigned_forms_refuse_a_negative(void)
{
unsigned long ulout = 99;
unsigned long long ullout = 99;
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_strtoul("-1", NULL, 10, &ulout),
AKERR_VALUE, "negative");
AKSL_CHECK(ulout == 0);
/* Including behind leading whitespace, which strtoul skips. */
AKSL_CHECK_STATUS(aksl_strtoul(" -1", NULL, 10, &ulout), AKERR_VALUE);
AKSL_CHECK_STATUS(aksl_strtoull("-1", NULL, 10, &ullout), AKERR_VALUE);
AKSL_CHECK(ullout == 0);
/* A leading '+' is fine: it is a sign, not a negation. */
AKSL_CHECK_OK(aksl_strtoul("+7", NULL, 10, &ulout));
AKSL_CHECK(ulout == 7);
return 0;
}
static int test_unsigned_boundaries_round_trip(void)
{
unsigned long ulout = 0;
unsigned long long ullout = 0;
char buf[64];
snprintf(buf, sizeof(buf), "%lu", ULONG_MAX);
AKSL_CHECK_OK(aksl_strtoul(buf, NULL, 10, &ulout));
AKSL_CHECK(ulout == ULONG_MAX);
snprintf(buf, sizeof(buf), "%llu", ULLONG_MAX);
AKSL_CHECK_OK(aksl_strtoull(buf, NULL, 10, &ullout));
AKSL_CHECK(ullout == ULLONG_MAX);
return 0;
}
/* ---------------------------------------------------------------------- */
/* Floating point */
/* ---------------------------------------------------------------------- */
static int test_floating_point_forms(void)
{
double dout = 0.0;
float fout = 0.0f;
long double ldout = 0.0L;
char *end = NULL;
AKSL_CHECK_OK(aksl_strtod("2.5", NULL, &dout));
AKSL_CHECK(dout == 2.5);
AKSL_CHECK_OK(aksl_strtof("2.5", NULL, &fout));
AKSL_CHECK(fout == 2.5f);
AKSL_CHECK_OK(aksl_strtold("2.5", NULL, &ldout));
AKSL_CHECK(ldout == 2.5L);
AKSL_CHECK_OK(aksl_strtod("2.5rest", &end, &dout));
AKSL_CHECK(dout == 2.5);
AKSL_CHECK(strcmp(end, "rest") == 0);
AKSL_CHECK_STATUS(aksl_strtod("2.5rest", NULL, &dout), AKERR_VALUE);
AKSL_CHECK_STATUS(aksl_strtof("nope", NULL, &fout), AKERR_VALUE);
AKSL_CHECK_STATUS(aksl_strtold("nope", NULL, &ldout), AKERR_VALUE);
/* float has a much smaller range than double, and says so. */
AKSL_CHECK_STATUS(aksl_strtof("1e300", NULL, &fout), ERANGE);
AKSL_CHECK(fout == 0.0f);
AKSL_CHECK_OK(aksl_strtod("1e300", NULL, &dout));
AKSL_CHECK(dout > 0.0);
return 0;
}
/* ---------------------------------------------------------------------- */
/* Argument validation */
/* ---------------------------------------------------------------------- */
static int test_every_form_rejects_null_arguments(void)
{
long lout = 0;
long long llout = 0;
unsigned long ulout = 0;
unsigned long long ullout = 0;
double dout = 0.0;
float fout = 0.0f;
long double ldout = 0.0L;
AKSL_CHECK_STATUS(aksl_strtol(NULL, NULL, 10, &lout), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strtol("1", NULL, 10, NULL), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strtoll(NULL, NULL, 10, &llout), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strtoll("1", NULL, 10, NULL), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strtoul(NULL, NULL, 10, &ulout), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strtoul("1", NULL, 10, NULL), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strtoull(NULL, NULL, 10, &ullout), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strtoull("1", NULL, 10, NULL), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strtod(NULL, NULL, &dout), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strtod("1", NULL, NULL), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strtof(NULL, NULL, &fout), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strtof("1", NULL, NULL), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strtold(NULL, NULL, &ldout), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strtold("1", NULL, NULL), AKERR_NULLPOINTER);
return 0;
}
int main(void)
{
int failures = 0;
akerr_init();
AKSL_RUN(failures, test_base_is_honoured);
AKSL_RUN(failures, test_base_zero_detects_the_prefix);
AKSL_RUN(failures, test_digits_outside_the_base_end_the_number);
AKSL_RUN(failures, test_invalid_base_is_rejected);
AKSL_RUN(failures, test_endptr_reports_where_parsing_stopped);
AKSL_RUN(failures, test_every_form_writes_the_endptr);
AKSL_RUN(failures, test_no_digits_is_an_error_even_with_an_endptr);
AKSL_RUN(failures, test_endptr_chains_across_a_list);
AKSL_RUN(failures, test_range_errors_per_type);
AKSL_RUN(failures, test_unsigned_forms_refuse_a_negative);
AKSL_RUN(failures, test_unsigned_boundaries_round_trip);
AKSL_RUN(failures, test_floating_point_forms);
AKSL_RUN(failures, test_every_form_rejects_null_arguments);
AKSL_REPORT(failures);
}

View File

@@ -1,102 +1,607 @@
/*
* Depth-first tree search.
* Tree traversal -- TODO.md section 1.8, complete.
*
* Previously this test shared one TreeSearchParams across all three searches
* without resetting it, so `steps` accumulated (7, then 14, then 21) and the
* second assertion failed -- `tree` was a red test on every run. Each search
* now gets its own params.
* The old version of this file counted steps, which cannot tell the three
* depth-first orders apart because all three visit all seven nodes -- and could
* not prove that AKERR_ITERATOR_BREAK stopped anything either, because it hid
* its target in tree[6], the last node visited in every depth-first order. Every
* traversal here records the node pointers it was handed, in order, and compares
* against the expected sequence.
*
* Caveat worth knowing when reading the step counts below: the value is hidden
* in tree[6], which is the last node visited in pre-, in- and post-order alike,
* so "7 steps" holds for all three orders and does not actually distinguish
* them -- nor does it prove that AKERR_ITERATOR_BREAK stopped anything (it does
* not; see tests/test_tree_iterate_break.c and TODO.md 2.1.3). Visit-order
* assertions that tell the three traversals apart are TODO.md section 1.8.
*/
#include "aksl_capture.h"
#define MAX_LEAVES 7
#define HIDDEN_VALUE ((void *)17336)
typedef struct TreeSearchParams
{
void *value;
int steps;
aksl_TreeNode *node;
} TreeSearchParams;
static akerr_ErrorContext AKERR_NOIGNORE *find_value(aksl_TreeNode *node, void *data)
{
TreeSearchParams *parms = NULL;
PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, node, AKERR_NULLPOINTER, "node");
FAIL_ZERO_RETURN(e, data, AKERR_NULLPOINTER, "data");
parms = (TreeSearchParams *)data;
parms->steps += 1;
if ( node->leaf == parms->value ) {
parms->node = node;
FAIL_RETURN(e, AKERR_ITERATOR_BREAK, "stop");
}
SUCCEED_RETURN(e);
}
/*
* Build the 3-level tree used by every case here, with the search value hidden
* in the bottom-right leaf.
* The 7-node tree used throughout:
*
* LEFT RIGHT
* TREE[0]
* +--------^^---------+
* | |
* TREE[1] TREE[2]
* +---^^---+ +---^^---+
* | | | |
*TREE[3] TREE[4] TREE[5] TREE[6]
* TREE[3] TREE[4] TREE[5] TREE[6]
*
* pre-order 0 1 3 4 2 5 6
* in-order 3 1 4 0 5 2 6
* post-order 3 4 1 5 6 2 0
* BFS 0 1 2 3 4 5 6
* BFS_RIGHT 0 2 1 6 5 4 3
*/
static void build_tree(aksl_TreeNode *tree, TreeSearchParams *parms)
#include "aksl_capture.h"
#define MAX_LEAVES 7
#define MAX_VISITS 32
typedef struct VisitLog
{
memset((void *)tree, 0x00, sizeof(aksl_TreeNode) * MAX_LEAVES);
int count;
aksl_TreeNode *seen[MAX_VISITS];
aksl_TreeNode *break_at; /* node to raise ITERATOR_BREAK on, or NULL */
aksl_TreeNode *fail_at; /* node to raise AKERR_VALUE on, or NULL */
} VisitLog;
static void visitlog_init(VisitLog *log)
{
memset((void *)log, 0x00, sizeof(VisitLog));
}
static akerr_ErrorContext AKERR_NOIGNORE *record_visit(aksl_TreeNode *node, void *data)
{
VisitLog *log = NULL;
PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, node, AKERR_NULLPOINTER, "node");
FAIL_ZERO_RETURN(e, data, AKERR_NULLPOINTER, "data");
log = (VisitLog *)data;
if ( log->count < MAX_VISITS ) {
log->seen[log->count] = node;
}
log->count += 1;
if ( log->fail_at == node ) {
FAIL_RETURN(e, AKERR_VALUE, "iterator failed at node %p", (void *)node);
}
if ( log->break_at == node ) {
FAIL_RETURN(e, AKERR_ITERATOR_BREAK, "stop at node %p", (void *)node);
}
SUCCEED_RETURN(e);
}
static void build_tree(aksl_TreeNode *tree)
{
int i = 0;
for ( i = 0; i < MAX_LEAVES; i++ ) {
memset((void *)&tree[i], 0x00, sizeof(aksl_TreeNode));
}
tree[0].left = &tree[1];
tree[0].right = &tree[2];
tree[1].left = &tree[3];
tree[1].right = &tree[4];
tree[2].left = &tree[5];
tree[2].right = &tree[6];
tree[6].leaf = HIDDEN_VALUE;
memset((void *)parms, 0x00, sizeof(TreeSearchParams));
parms->value = HIDDEN_VALUE;
}
static int search_finds_hidden_value(uint8_t searchmode)
/* Assert that the visit log matches `expected`, which is a list of tree indices. */
static int check_order(VisitLog *log, aksl_TreeNode *tree,
const int *expected, int n)
{
aksl_TreeNode tree[MAX_LEAVES];
TreeSearchParams parms;
int i = 0;
build_tree(tree, &parms);
AKSL_CHECK_OK(aksl_tree_iterate(&tree[0], &find_value, NULL, NULL,
searchmode, &parms, NULL));
AKSL_CHECK(parms.node == &tree[6]);
AKSL_CHECK(parms.steps == MAX_LEAVES);
if ( log->count != n ) {
fprintf(stderr, " CHECK FAILED: visited %d nodes, expected %d\n",
log->count, n);
return 1;
}
for ( i = 0; i < n; i++ ) {
if ( log->seen[i] != &tree[expected[i]] ) {
fprintf(stderr, " CHECK FAILED: visit %d was node %ld, expected %d\n",
i, (long)(log->seen[i] - tree), expected[i]);
return 1;
}
}
return 0;
}
static int test_dfs_preorder(void)
/* ---------------------------------------------------------------------- */
/* aksl_tree_node_init */
/* ---------------------------------------------------------------------- */
static int test_node_init_zeroes_the_links(void)
{
return search_finds_hidden_value(AKSL_TREE_SEARCH_DFS_PREORDER);
aksl_TreeNode node;
int payload = 3;
memset((void *)&node, 0xff, sizeof(node));
AKSL_CHECK_OK(aksl_tree_node_init(&node, &payload));
AKSL_CHECK(node.parent == NULL);
AKSL_CHECK(node.left == NULL);
AKSL_CHECK(node.right == NULL);
AKSL_CHECK(node.leaf == (void *)&payload);
AKSL_CHECK_STATUS(aksl_tree_node_init(NULL, NULL), AKERR_NULLPOINTER);
return 0;
}
static int test_dfs_inorder(void)
/* ---------------------------------------------------------------------- */
/* Traversal orders */
/* ---------------------------------------------------------------------- */
static int test_preorder_visits_root_left_right(void)
{
return search_finds_hidden_value(AKSL_TREE_SEARCH_DFS_INORDER);
aksl_TreeNode tree[MAX_LEAVES];
VisitLog log;
static const int expected[MAX_LEAVES] = { 0, 1, 3, 4, 2, 5, 6 };
build_tree(tree);
visitlog_init(&log);
AKSL_CHECK_OK(aksl_tree_iterate(&tree[0], &record_visit, NULL, NULL,
AKSL_TREE_SEARCH_DFS_PREORDER, &log));
AKSL_CHECK(check_order(&log, tree, expected, MAX_LEAVES) == 0);
return 0;
}
static int test_dfs_postorder(void)
static int test_inorder_visits_left_root_right(void)
{
return search_finds_hidden_value(AKSL_TREE_SEARCH_DFS_POSTORDER);
aksl_TreeNode tree[MAX_LEAVES];
VisitLog log;
static const int expected[MAX_LEAVES] = { 3, 1, 4, 0, 5, 2, 6 };
build_tree(tree);
visitlog_init(&log);
AKSL_CHECK_OK(aksl_tree_iterate(&tree[0], &record_visit, NULL, NULL,
AKSL_TREE_SEARCH_DFS_INORDER, &log));
AKSL_CHECK(check_order(&log, tree, expected, MAX_LEAVES) == 0);
return 0;
}
static int test_postorder_visits_left_right_root(void)
{
aksl_TreeNode tree[MAX_LEAVES];
VisitLog log;
static const int expected[MAX_LEAVES] = { 3, 4, 1, 5, 6, 2, 0 };
build_tree(tree);
visitlog_init(&log);
AKSL_CHECK_OK(aksl_tree_iterate(&tree[0], &record_visit, NULL, NULL,
AKSL_TREE_SEARCH_DFS_POSTORDER, &log));
AKSL_CHECK(check_order(&log, tree, expected, MAX_LEAVES) == 0);
return 0;
}
/* AKSL_TREE_SEARCH_DFS is documented as an alias for the pre-order mode. */
static int test_dfs_is_an_alias_for_preorder(void)
{
aksl_TreeNode tree[MAX_LEAVES];
VisitLog log;
static const int expected[MAX_LEAVES] = { 0, 1, 3, 4, 2, 5, 6 };
build_tree(tree);
visitlog_init(&log);
AKSL_CHECK_OK(aksl_tree_iterate(&tree[0], &record_visit, NULL, NULL,
AKSL_TREE_SEARCH_DFS, &log));
AKSL_CHECK(check_order(&log, tree, expected, MAX_LEAVES) == 0);
return 0;
}
/*
* BFS was AKERR_NOT_IMPLEMENTED, and the lalloc/lfree parameters that existed to
* serve it were defaulted and then never called -- TODO.md 2.2.8 and 2.2.10.
* Both modes work now, and the allocator test below proves the queue is real.
*/
static int test_bfs_visits_level_by_level(void)
{
aksl_TreeNode tree[MAX_LEAVES];
VisitLog log;
static const int expected[MAX_LEAVES] = { 0, 1, 2, 3, 4, 5, 6 };
build_tree(tree);
visitlog_init(&log);
AKSL_CHECK_OK(aksl_tree_iterate(&tree[0], &record_visit, NULL, NULL,
AKSL_TREE_SEARCH_BFS, &log));
AKSL_CHECK(check_order(&log, tree, expected, MAX_LEAVES) == 0);
return 0;
}
static int test_bfs_right_visits_right_child_first(void)
{
aksl_TreeNode tree[MAX_LEAVES];
VisitLog log;
static const int expected[MAX_LEAVES] = { 0, 2, 1, 6, 5, 4, 3 };
build_tree(tree);
visitlog_init(&log);
AKSL_CHECK_OK(aksl_tree_iterate(&tree[0], &record_visit, NULL, NULL,
AKSL_TREE_SEARCH_BFS_RIGHT, &log));
AKSL_CHECK(check_order(&log, tree, expected, MAX_LEAVES) == 0);
return 0;
}
/* AKSL_TREE_SEARCH_VISIT: this node and no further. */
static int test_visit_mode_does_not_descend(void)
{
aksl_TreeNode tree[MAX_LEAVES];
VisitLog log;
static const int expected[1] = { 0 };
build_tree(tree);
visitlog_init(&log);
AKSL_CHECK_OK(aksl_tree_iterate(&tree[0], &record_visit, NULL, NULL,
AKSL_TREE_SEARCH_VISIT, &log));
AKSL_CHECK(check_order(&log, tree, expected, 1) == 0);
return 0;
}
/* ---------------------------------------------------------------------- */
/* Custom allocator */
/* ---------------------------------------------------------------------- */
static int counting_alloc_calls = 0;
static int counting_free_calls = 0;
static akerr_ErrorContext AKERR_NOIGNORE *counting_alloc(size_t size, void **dest)
{
counting_alloc_calls += 1;
return aksl_malloc(size, dest);
}
static akerr_ErrorContext AKERR_NOIGNORE *counting_free(void *ptr)
{
counting_free_calls += 1;
return aksl_free(ptr);
}
/*
* TODO.md 1.8: "Custom lalloc/lfree are actually invoked -- currently they are
* stored and never called". They are called now, once per node enqueued, and
* every allocation is released. The depth-first modes allocate nothing at all,
* which is the other half of the contract.
*/
static int test_custom_allocator_is_used_and_balanced(void)
{
aksl_TreeNode tree[MAX_LEAVES];
VisitLog log;
build_tree(tree);
visitlog_init(&log);
counting_alloc_calls = 0;
counting_free_calls = 0;
AKSL_CHECK_OK(aksl_tree_iterate(&tree[0], &record_visit,
&counting_alloc, &counting_free,
AKSL_TREE_SEARCH_BFS, &log));
AKSL_CHECK(log.count == MAX_LEAVES);
/* One queue entry per node visited, and every one of them released. */
AKSL_CHECK(counting_alloc_calls == MAX_LEAVES);
AKSL_CHECK(counting_free_calls == MAX_LEAVES);
/* Depth-first needs no queue, so it must not touch the allocator. */
counting_alloc_calls = 0;
counting_free_calls = 0;
visitlog_init(&log);
AKSL_CHECK_OK(aksl_tree_iterate(&tree[0], &record_visit,
&counting_alloc, &counting_free,
AKSL_TREE_SEARCH_DFS_PREORDER, &log));
AKSL_CHECK(counting_alloc_calls == 0);
AKSL_CHECK(counting_free_calls == 0);
return 0;
}
/* A break part-way through a BFS still drains the queue it had already built. */
static int test_break_during_bfs_releases_the_queue(void)
{
aksl_TreeNode tree[MAX_LEAVES];
VisitLog log;
build_tree(tree);
visitlog_init(&log);
log.break_at = &tree[1];
counting_alloc_calls = 0;
counting_free_calls = 0;
AKSL_CHECK_OK(aksl_tree_iterate(&tree[0], &record_visit,
&counting_alloc, &counting_free,
AKSL_TREE_SEARCH_BFS, &log));
/* 0 and 1 visited; the walk stops at 1 with 2 still sitting in the queue. */
AKSL_CHECK(log.count == 2);
AKSL_CHECK(counting_alloc_calls > 0);
AKSL_CHECK(counting_alloc_calls == counting_free_calls);
return 0;
}
/* ---------------------------------------------------------------------- */
/* Degenerate shapes */
/* ---------------------------------------------------------------------- */
static int test_single_node_tree_visits_once_in_every_order(void)
{
aksl_TreeNode node;
VisitLog log;
static const uint8_t modes[] = {
AKSL_TREE_SEARCH_DFS_PREORDER,
AKSL_TREE_SEARCH_DFS_INORDER,
AKSL_TREE_SEARCH_DFS_POSTORDER,
AKSL_TREE_SEARCH_BFS,
AKSL_TREE_SEARCH_BFS_RIGHT,
AKSL_TREE_SEARCH_VISIT,
};
size_t i = 0;
for ( i = 0; i < sizeof(modes) / sizeof(modes[0]); i++ ) {
AKSL_CHECK_OK(aksl_tree_node_init(&node, NULL));
visitlog_init(&log);
AKSL_CHECK_OK(aksl_tree_iterate(&node, &record_visit, NULL, NULL,
modes[i], &log));
AKSL_CHECK(log.count == 1);
AKSL_CHECK(log.seen[0] == &node);
}
return 0;
}
/* Left-only and right-only chains of three nodes, no branching anywhere. */
static int test_degenerate_chains(void)
{
aksl_TreeNode chain[3];
VisitLog log;
static const int forward[3] = { 0, 1, 2 };
static const int backward[3] = { 2, 1, 0 };
int i = 0;
/* Left-only: 0 -> 1 -> 2 */
for ( i = 0; i < 3; i++ ) {
AKSL_CHECK_OK(aksl_tree_node_init(&chain[i], NULL));
}
chain[0].left = &chain[1];
chain[1].left = &chain[2];
visitlog_init(&log);
AKSL_CHECK_OK(aksl_tree_iterate(&chain[0], &record_visit, NULL, NULL,
AKSL_TREE_SEARCH_DFS_PREORDER, &log));
AKSL_CHECK(check_order(&log, chain, forward, 3) == 0);
/* In-order down a left chain arrives at the deepest node first. */
visitlog_init(&log);
AKSL_CHECK_OK(aksl_tree_iterate(&chain[0], &record_visit, NULL, NULL,
AKSL_TREE_SEARCH_DFS_INORDER, &log));
AKSL_CHECK(check_order(&log, chain, backward, 3) == 0);
/* Right-only: the same shape, mirrored. */
for ( i = 0; i < 3; i++ ) {
AKSL_CHECK_OK(aksl_tree_node_init(&chain[i], NULL));
}
chain[0].right = &chain[1];
chain[1].right = &chain[2];
visitlog_init(&log);
AKSL_CHECK_OK(aksl_tree_iterate(&chain[0], &record_visit, NULL, NULL,
AKSL_TREE_SEARCH_DFS_INORDER, &log));
AKSL_CHECK(check_order(&log, chain, forward, 3) == 0);
visitlog_init(&log);
AKSL_CHECK_OK(aksl_tree_iterate(&chain[0], &record_visit, NULL, NULL,
AKSL_TREE_SEARCH_BFS, &log));
AKSL_CHECK(check_order(&log, chain, forward, 3) == 0);
return 0;
}
/*
* TODO.md 1.8 / 2.2.7: a chain deeper than the recursion can take. It used to
* overflow the stack; it is AKERR_OUTOFBOUNDS now, and the message names the
* documented limit. Built one node past the cap so the failure is the cap itself
* and not some incidental shortfall. `static` because AKSL_TREE_MAX_DEPTH nodes
* on the stack of a test function is not the point of the test.
*/
static int test_tree_deeper_than_the_cap_is_out_of_bounds(void)
{
static aksl_TreeNode chain[AKSL_TREE_MAX_DEPTH + 2];
VisitLog log;
int i = 0;
for ( i = 0; i < AKSL_TREE_MAX_DEPTH + 2; i++ ) {
AKSL_CHECK_OK(aksl_tree_node_init(&chain[i], NULL));
}
for ( i = 0; i < AKSL_TREE_MAX_DEPTH + 1; i++ ) {
chain[i].left = &chain[i + 1];
}
visitlog_init(&log);
AKSL_CHECK_STATUS_MSG_CONTAINS(
aksl_tree_iterate(&chain[0], &record_visit, NULL, NULL,
AKSL_TREE_SEARCH_DFS_PREORDER, &log),
AKERR_OUTOFBOUNDS, "AKSL_TREE_MAX_DEPTH");
/* Exactly at the cap is fine -- a limit, not an off-by-one. */
chain[AKSL_TREE_MAX_DEPTH - 1].left = NULL;
visitlog_init(&log);
AKSL_CHECK_OK(aksl_tree_iterate(&chain[0], &record_visit, NULL, NULL,
AKSL_TREE_SEARCH_DFS_PREORDER, &log));
AKSL_CHECK(log.count == AKSL_TREE_MAX_DEPTH);
/*
* And again leaning right. The recursion counts depth separately for each
* child, so a chain that only ever goes left says nothing about whether the
* right-hand descent counts at all -- mutation testing found exactly that:
* changing the right child's `depth + 1` to `depth + 0` survived the whole
* suite, because nothing here had ever recursed right more than three deep.
*/
for ( i = 0; i < AKSL_TREE_MAX_DEPTH + 2; i++ ) {
AKSL_CHECK_OK(aksl_tree_node_init(&chain[i], NULL));
}
for ( i = 0; i < AKSL_TREE_MAX_DEPTH + 1; i++ ) {
chain[i].right = &chain[i + 1];
}
visitlog_init(&log);
AKSL_CHECK_STATUS(aksl_tree_iterate(&chain[0], &record_visit, NULL, NULL,
AKSL_TREE_SEARCH_DFS_PREORDER, &log),
AKERR_OUTOFBOUNDS);
/* Both orders that descend right, so in-order and post-order count too. */
visitlog_init(&log);
AKSL_CHECK_STATUS(aksl_tree_iterate(&chain[0], &record_visit, NULL, NULL,
AKSL_TREE_SEARCH_DFS_INORDER, &log),
AKERR_OUTOFBOUNDS);
visitlog_init(&log);
AKSL_CHECK_STATUS(aksl_tree_iterate(&chain[0], &record_visit, NULL, NULL,
AKSL_TREE_SEARCH_DFS_POSTORDER, &log),
AKERR_OUTOFBOUNDS);
/* And breadth-first, whose depth is carried on the queue entry instead. */
visitlog_init(&log);
AKSL_CHECK_STATUS(aksl_tree_iterate(&chain[0], &record_visit, NULL, NULL,
AKSL_TREE_SEARCH_BFS, &log),
AKERR_OUTOFBOUNDS);
return 0;
}
/*
* A child pointing back at an ancestor. This used to recurse until the process
* died. The depth-first walk carries the ancestor chain and recognises the node
* as its own forebear; the breadth-first walk has no ancestor chain to compare
* against, so the same tree comes back as AKERR_OUTOFBOUNDS through the depth
* cap instead -- a different status for the same shape, which the header says
* out loud rather than leaving to be discovered.
*/
static int test_cyclic_tree_is_caught(void)
{
aksl_TreeNode tree[MAX_LEAVES];
VisitLog log;
build_tree(tree);
tree[3].left = &tree[0]; /* back to the root */
visitlog_init(&log);
AKSL_CHECK_STATUS_MSG_CONTAINS(
aksl_tree_iterate(&tree[0], &record_visit, NULL, NULL,
AKSL_TREE_SEARCH_DFS_PREORDER, &log),
AKERR_CIRCULAR_REFERENCE, "own ancestor");
visitlog_init(&log);
AKSL_CHECK_STATUS(aksl_tree_iterate(&tree[0], &record_visit, NULL, NULL,
AKSL_TREE_SEARCH_BFS, &log),
AKERR_OUTOFBOUNDS);
return 0;
}
/* ---------------------------------------------------------------------- */
/* Callback control flow */
/* ---------------------------------------------------------------------- */
/*
* The defect that made ITERATOR_BREAK useless on a tree: the recursive frame
* that raised the break handled it in its own PROCESS/HANDLE block and returned
* success, so the parent frame's PASS saw nothing wrong and carried straight on
* into the sibling subtree. All seven nodes were visited no matter where the
* break was raised. TODO.md 2.1.3.
*
* One case per order, each breaking on a node that is *not* last in that order --
* which is precisely what the old test could not do, because it hid its target
* in tree[6], the final node in all three depth-first walks.
*/
static int test_break_aborts_the_whole_traversal(void)
{
aksl_TreeNode tree[MAX_LEAVES];
VisitLog log;
build_tree(tree);
/* Pre-order 0 1 3 ... : breaking at 3 is the third visit. */
visitlog_init(&log);
log.break_at = &tree[3];
AKSL_CHECK_OK(aksl_tree_iterate(&tree[0], &record_visit, NULL, NULL,
AKSL_TREE_SEARCH_DFS_PREORDER, &log));
AKSL_CHECK(log.count == 3);
/* In-order 3 1 4 ... : breaking at 4 is the third visit. */
visitlog_init(&log);
log.break_at = &tree[4];
AKSL_CHECK_OK(aksl_tree_iterate(&tree[0], &record_visit, NULL, NULL,
AKSL_TREE_SEARCH_DFS_INORDER, &log));
AKSL_CHECK(log.count == 3);
/* Post-order 3 4 1 5 ... : breaking at 5 is the fourth visit. */
visitlog_init(&log);
log.break_at = &tree[5];
AKSL_CHECK_OK(aksl_tree_iterate(&tree[0], &record_visit, NULL, NULL,
AKSL_TREE_SEARCH_DFS_POSTORDER, &log));
AKSL_CHECK(log.count == 4);
/* BFS 0 1 2 3 ... : breaking at 2 is the third visit. */
visitlog_init(&log);
log.break_at = &tree[2];
AKSL_CHECK_OK(aksl_tree_iterate(&tree[0], &record_visit, NULL, NULL,
AKSL_TREE_SEARCH_BFS, &log));
AKSL_CHECK(log.count == 3);
return 0;
}
/* Any other status propagates out with its message and its own status intact. */
static int test_callback_error_propagates(void)
{
aksl_TreeNode tree[MAX_LEAVES];
VisitLog log;
build_tree(tree);
visitlog_init(&log);
log.fail_at = &tree[3];
AKSL_CHECK_STATUS_MSG_CONTAINS(
aksl_tree_iterate(&tree[0], &record_visit, NULL, NULL,
AKSL_TREE_SEARCH_DFS_PREORDER, &log),
AKERR_VALUE, "iterator failed at node");
AKSL_CHECK(log.count == 3);
/* And out of a BFS, where the queue has to be drained on the way past. */
visitlog_init(&log);
log.fail_at = &tree[2];
AKSL_CHECK_STATUS(aksl_tree_iterate(&tree[0], &record_visit, NULL, NULL,
AKSL_TREE_SEARCH_BFS, &log),
AKERR_VALUE);
return 0;
}
/* ---------------------------------------------------------------------- */
/* Argument validation */
/* ---------------------------------------------------------------------- */
static int test_null_arguments(void)
{
aksl_TreeNode tree[MAX_LEAVES];
VisitLog log;
build_tree(tree);
visitlog_init(&log);
AKSL_CHECK_STATUS(aksl_tree_iterate(NULL, &record_visit, NULL, NULL,
AKSL_TREE_SEARCH_DFS_PREORDER, &log),
AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_tree_iterate(&tree[0], NULL, NULL, NULL,
AKSL_TREE_SEARCH_DFS_PREORDER, &log),
AKERR_NULLPOINTER);
AKSL_CHECK(log.count == 0);
return 0;
}
/*
* TODO.md 2.2.9: the switch had no default, so an unrecognised mode -- and
* AKSL_TREE_SEARCH_VISIT, which the header documented but nothing implemented --
* fell straight through to SUCCEED_RETURN having visited nothing at all. A
* traversal that silently did not happen, reported as success.
*/
static int test_unknown_searchmode_is_a_value_error(void)
{
aksl_TreeNode tree[MAX_LEAVES];
VisitLog log;
build_tree(tree);
visitlog_init(&log);
AKSL_CHECK_STATUS_MSG_CONTAINS(
aksl_tree_iterate(&tree[0], &record_visit, NULL, NULL, 99, &log),
AKERR_VALUE, "unknown searchmode");
AKSL_CHECK(log.count == 0);
/* 6 is one past the last defined mode, and just as unacceptable. */
AKSL_CHECK_STATUS(aksl_tree_iterate(&tree[0], &record_visit, NULL, NULL, 6, &log),
AKERR_VALUE);
AKSL_CHECK(log.count == 0);
return 0;
}
int main(void)
@@ -105,9 +610,29 @@ int main(void)
akerr_init();
AKSL_RUN(failures, test_dfs_preorder);
AKSL_RUN(failures, test_dfs_inorder);
AKSL_RUN(failures, test_dfs_postorder);
AKSL_RUN(failures, test_node_init_zeroes_the_links);
AKSL_RUN(failures, test_preorder_visits_root_left_right);
AKSL_RUN(failures, test_inorder_visits_left_root_right);
AKSL_RUN(failures, test_postorder_visits_left_right_root);
AKSL_RUN(failures, test_dfs_is_an_alias_for_preorder);
AKSL_RUN(failures, test_bfs_visits_level_by_level);
AKSL_RUN(failures, test_bfs_right_visits_right_child_first);
AKSL_RUN(failures, test_visit_mode_does_not_descend);
AKSL_RUN(failures, test_custom_allocator_is_used_and_balanced);
AKSL_RUN(failures, test_break_during_bfs_releases_the_queue);
AKSL_RUN(failures, test_single_node_tree_visits_once_in_every_order);
AKSL_RUN(failures, test_degenerate_chains);
AKSL_RUN(failures, test_tree_deeper_than_the_cap_is_out_of_bounds);
AKSL_RUN(failures, test_cyclic_tree_is_caught);
AKSL_RUN(failures, test_break_aborts_the_whole_traversal);
AKSL_RUN(failures, test_callback_error_propagates);
AKSL_RUN(failures, test_null_arguments);
AKSL_RUN(failures, test_unknown_searchmode_is_a_value_error);
AKSL_REPORT(failures);
}

View File

@@ -1,89 +0,0 @@
/*
* KNOWN FAILING -- TODO.md section 2.1.3
*
* AKERR_ITERATOR_BREAK does not abort a tree traversal. aksl_tree_iterate
* recurses into itself, and the frame in which the callback raises the break
* handles it in its own PROCESS/HANDLE(e, AKERR_ITERATOR_BREAK) block and
* returns *success*. The parent frame's PASS therefore sees no error at all and
* carries straight on into the sibling subtree, so the traversal runs to
* completion.
*
* The 7-node tree below is walked pre-order (0, 1, 3, 4, 2, 5, 6) with the
* callback breaking on tree[3], the third node visited. A working break stops
* the walk at 3 visits; today the callback is invoked all 7 times.
*
* tests/test_tree.c cannot catch this: it hides its value in tree[6], which is
* the last node visited in all three depth-first orders, so a break that never
* fires is indistinguishable from one that fires on the final node.
*
* Registered in AKSL_KNOWN_FAILING_TESTS (WILL_FAIL). When the break semantics
* are fixed, CTest reports this as unexpectedly passing -- move it into
* AKSL_TESTS then.
*/
#include "aksl_capture.h"
#define MAX_LEAVES 7
typedef struct BreakParams
{
aksl_TreeNode *stop_at;
int visits;
} BreakParams;
static akerr_ErrorContext AKERR_NOIGNORE *break_at_node(aksl_TreeNode *node, void *data)
{
BreakParams *parms = NULL;
PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, node, AKERR_NULLPOINTER, "node");
FAIL_ZERO_RETURN(e, data, AKERR_NULLPOINTER, "data");
parms = (BreakParams *)data;
parms->visits += 1;
if ( node == parms->stop_at ) {
FAIL_RETURN(e, AKERR_ITERATOR_BREAK, "stop");
}
SUCCEED_RETURN(e);
}
static int test_break_aborts_the_whole_traversal(void)
{
aksl_TreeNode tree[MAX_LEAVES];
BreakParams parms;
memset((void *)tree, 0x00, sizeof(tree));
memset((void *)&parms, 0x00, sizeof(parms));
/*
* TREE[0]
* +--------^^---------+
* | |
* TREE[1] TREE[2]
* +---^^---+ +---^^---+
* | | | |
*TREE[3] TREE[4] TREE[5] TREE[6]
*/
tree[0].left = &tree[1];
tree[0].right = &tree[2];
tree[1].left = &tree[3];
tree[1].right = &tree[4];
tree[2].left = &tree[5];
tree[2].right = &tree[6];
parms.stop_at = &tree[3];
AKSL_CHECK_OK(aksl_tree_iterate(&tree[0], &break_at_node, NULL, NULL,
AKSL_TREE_SEARCH_DFS_PREORDER, &parms, NULL));
/* Pre-order visits 0, 1, 3 -- the break on tree[3] must stop the walk there. */
AKSL_CHECK(parms.visits == 3);
return 0;
}
int main(void)
{
int failures = 0;
akerr_init();
AKSL_RUN(failures, test_break_aborts_the_whole_traversal);
AKSL_REPORT(failures);
}

177
tests/test_version.c Normal file
View File

@@ -0,0 +1,177 @@
/*
* Version reporting -- TODO.md section 2.2.16.
*
* There are two versions in play and the whole point of this API is that they
* are allowed to differ:
*
* the AKSL_VERSION_* macros what the caller was COMPILED against, baked
* into the caller's object file from whatever
* akstdlib_version.h was on its include path
* aksl_version() and friends what is actually LOADED, baked into
* libakstdlib.so when the library was built
*
* In this test they necessarily agree, because the test binary and the library
* are built from one tree in one configure. So the assertions below split into
* two kinds: the ones that check the two sides agree (which would catch a build
* that compiled the library and its tests against different generated headers),
* and the ones that drive aksl_version_check() with deliberately wrong numbers
* to prove it actually refuses a mismatch rather than always returning success.
* The second kind is what makes the first kind worth anything.
*/
#include "aksl_capture.h"
static int test_version_string_matches_its_components(void)
{
char expected[64];
snprintf(expected, sizeof(expected), "%d.%d.%d",
AKSL_VERSION_MAJOR, AKSL_VERSION_MINOR, AKSL_VERSION_PATCH);
AKSL_CHECK(strcmp(AKSL_VERSION_STRING, expected) == 0);
return 0;
}
/*
* AKSL_VERSION_NUMBER must be ordinary decimal arithmetic. Written out as a
* literal it would be tempting to spell 0.1.0 as 000100, which C reads as
* octal 64 -- so this compares the compile-time macro against the same figure
* computed at runtime from the loaded library's own components. A literal that
* went octal fails here; the current computed form cannot.
*/
static int test_version_number_is_decimal_and_ordered(void)
{
int major = -1;
int minor = -1;
int patch = -1;
AKSL_CHECK_OK(aksl_version(&major, &minor, &patch));
AKSL_CHECK(AKSL_VERSION_NUMBER == (major * 10000) + (minor * 100) + patch);
/* Usable in the preprocessor, which is the reason it exists at all. */
#if AKSL_VERSION_NUMBER < 0
AKSL_CHECK(0 && "AKSL_VERSION_NUMBER is negative");
#endif
AKSL_CHECK(AKSL_VERSION_NUMBER >= 0);
return 0;
}
/* Header and shared library came out of the same configure. */
static int test_loaded_version_matches_the_header(void)
{
int major = -1;
int minor = -1;
int patch = -1;
AKSL_CHECK_OK(aksl_version(&major, &minor, &patch));
AKSL_CHECK(major == AKSL_VERSION_MAJOR);
AKSL_CHECK(minor == AKSL_VERSION_MINOR);
AKSL_CHECK(patch == AKSL_VERSION_PATCH);
AKSL_CHECK(strcmp(aksl_version_string(), AKSL_VERSION_STRING) == 0);
AKSL_CHECK(strcmp(aksl_version_soname(), AKSL_VERSION_SONAME) == 0);
return 0;
}
/*
* The soname is the ABI-break granularity, and it mirrors the SOVERSION
* expression in CMakeLists.txt: MAJOR.MINOR while major is 0, MAJOR alone after
* that. If one of the two ever changes without the other, this fails.
*/
static int test_soname_matches_the_documented_rule(void)
{
char expected[64];
#if AKSL_VERSION_MAJOR == 0
snprintf(expected, sizeof(expected), "%d.%d",
AKSL_VERSION_MAJOR, AKSL_VERSION_MINOR);
#else
snprintf(expected, sizeof(expected), "%d", AKSL_VERSION_MAJOR);
#endif
AKSL_CHECK(strcmp(aksl_version_soname(), expected) == 0);
return 0;
}
static int test_version_check_accepts_a_matching_build(void)
{
AKSL_CHECK_OK(AKSL_VERSION_CHECK());
return 0;
}
/*
* The refusals. Without these the success above proves nothing -- a
* aksl_version_check() that returned NULL unconditionally would pass it.
*/
static int test_version_check_refuses_a_mismatch(void)
{
AKSL_CHECK_STATUS(aksl_version_check(AKSL_VERSION_MAJOR + 1,
AKSL_VERSION_MINOR,
AKSL_VERSION_PATCH),
AKERR_VALUE);
AKSL_CHECK_STATUS(aksl_version_check(AKSL_VERSION_MAJOR,
AKSL_VERSION_MINOR + 1,
AKSL_VERSION_PATCH),
AKERR_VALUE);
return 0;
}
/*
* Patch is deliberately not part of the comparison: a patch bump is an ABI
* promise, so a caller built against 0.1.0 must keep working against 0.1.7.
*/
static int test_version_check_ignores_the_patch_level(void)
{
AKSL_CHECK_OK(aksl_version_check(AKSL_VERSION_MAJOR,
AKSL_VERSION_MINOR,
AKSL_VERSION_PATCH + 9));
return 0;
}
/* The error has to say which two versions disagree, or it is not actionable. */
static int test_mismatch_message_names_both_versions(void)
{
char compiled[64];
snprintf(compiled, sizeof(compiled), "%d.%d.%d",
AKSL_VERSION_MAJOR + 1, AKSL_VERSION_MINOR, AKSL_VERSION_PATCH);
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_version_check(AKSL_VERSION_MAJOR + 1,
AKSL_VERSION_MINOR,
AKSL_VERSION_PATCH),
AKERR_VALUE, compiled);
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_version_check(AKSL_VERSION_MAJOR + 1,
AKSL_VERSION_MINOR,
AKSL_VERSION_PATCH),
AKERR_VALUE, AKSL_VERSION_STRING);
return 0;
}
static int test_rejects_null_arguments(void)
{
int major = 0;
int minor = 0;
int patch = 0;
AKSL_CHECK_STATUS(aksl_version(NULL, &minor, &patch), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_version(&major, NULL, &patch), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_version(&major, &minor, NULL), AKERR_NULLPOINTER);
return 0;
}
int main(void)
{
int failures = 0;
akerr_init();
AKSL_RUN(failures, test_version_string_matches_its_components);
AKSL_RUN(failures, test_version_number_is_decimal_and_ordered);
AKSL_RUN(failures, test_loaded_version_matches_the_header);
AKSL_RUN(failures, test_soname_matches_the_documented_rule);
AKSL_RUN(failures, test_version_check_accepts_a_matching_build);
AKSL_RUN(failures, test_version_check_refuses_a_mismatch);
AKSL_RUN(failures, test_version_check_ignores_the_patch_level);
AKSL_RUN(failures, test_mismatch_message_names_both_versions);
AKSL_RUN(failures, test_rejects_null_arguments);
AKSL_REPORT(failures);
}