Compare commits
23 Commits
9
...
f8425b8729
| Author | SHA1 | Date | |
|---|---|---|---|
|
f8425b8729
|
|||
|
cc5e7899bb
|
|||
|
3ad3994762
|
|||
|
7940276f87
|
|||
|
b104a07eb4
|
|||
|
125eeb2109
|
|||
|
98a0a8562d
|
|||
|
55eb0334c4
|
|||
|
fd71bcc67b
|
|||
|
95e5002512
|
|||
|
07c448508b
|
|||
|
a37ba3fb89
|
|||
|
437da2960b
|
|||
|
82c47ed773
|
|||
|
a87cbfb26d
|
|||
|
566004afd6
|
|||
|
11c04923f8
|
|||
|
01734f511b
|
|||
| 8003239116 | |||
|
80205d5c4f
|
|||
|
54fb44cd2b
|
|||
|
68009ea0e3
|
|||
|
3e296c3bff
|
@@ -21,7 +21,7 @@ jobs:
|
||||
# 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.
|
||||
# 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
|
||||
@@ -155,7 +155,7 @@ jobs:
|
||||
# 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 issue #7; raise the gate as
|
||||
# The survivors worth acting on are named in TODO.md; raise the gate as
|
||||
# they become assertions.
|
||||
- name: mutation testing
|
||||
run: |
|
||||
|
||||
36
AGENTS.md
36
AGENTS.md
@@ -76,10 +76,6 @@ 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:
|
||||
|
||||
- **Preserve the libc contract unless there is a compelling, documented reason
|
||||
not to.** libc behaviour is the standard to meet. This library changes only
|
||||
the error transport (to `akerror`) and, where libc returns a value, the result
|
||||
shape (through a caller-provided destination pointer).
|
||||
- **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.
|
||||
@@ -97,19 +93,13 @@ 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.
|
||||
|
||||
**Do not wrap a libc function that cannot fail and provides no failure or
|
||||
operation-status result.** There is no `akerror` context to carry. A value such
|
||||
as `umask()`'s previous mask is not an operation-status result, so `umask()` is
|
||||
not a wrapper candidate.
|
||||
|
||||
## 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
|
||||
`AKSL_KNOWN_FAILING_TESTS` assert documented defects from `TODO.md`; when one
|
||||
starts unexpectedly passing, move it into `AKSL_TESTS` with the fix. Both of the
|
||||
latter are currently empty -- all six confirmed defects are fixed -- but the
|
||||
mechanism stays for the next one.
|
||||
|
||||
@@ -129,7 +119,7 @@ file, `find_dependency(akerror)` resolving, and the exported
|
||||
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
|
||||
listing before proposing a change. Tests for behaviour that `TODO.md` 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.
|
||||
@@ -140,26 +130,10 @@ 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.
|
||||
link the relevant `TODO.md` item or issue when fixing a known defect.
|
||||
|
||||
## 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.
|
||||
`README.md` or `TODO.md` when changing documented workflows or known failures.
|
||||
|
||||
@@ -5,7 +5,7 @@ cmake_minimum_required(VERSION 3.10)
|
||||
# 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 listed in UPGRADING.md changed documented behaviour and, in five places,
|
||||
# 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
|
||||
@@ -54,7 +54,7 @@ if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
|
||||
endif()
|
||||
|
||||
# Warnings. The only warning in the tree when these went on was the unused
|
||||
# `queue` parameter of aksl_tree_iterate, which UPGRADING.md had already
|
||||
# `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.
|
||||
#
|
||||
@@ -63,7 +63,7 @@ endif()
|
||||
# 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
|
||||
# libakerror issue #15 -- so the code is pedantic-clean; it is the expansion that is not.
|
||||
# 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
|
||||
@@ -78,7 +78,7 @@ 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 listed in UPGRADING.md (the uninitialised %s in
|
||||
# several of the defects in TODO.md section 2 (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)
|
||||
@@ -173,7 +173,7 @@ if(CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR)
|
||||
# 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 -- libakerror issue #15.
|
||||
# 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})
|
||||
@@ -207,7 +207,6 @@ add_library(akstdlib SHARED
|
||||
src/stdlib.c
|
||||
src/string.c
|
||||
src/stream.c
|
||||
src/stat.c
|
||||
src/collections.c
|
||||
)
|
||||
|
||||
@@ -299,7 +298,7 @@ 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; see UPGRADING.md. They fail until
|
||||
# defect from TODO.md section 2.1. 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
|
||||
@@ -318,7 +317,6 @@ set(AKSL_TESTS
|
||||
strbuf
|
||||
stream
|
||||
streamio
|
||||
stat
|
||||
strhash
|
||||
string
|
||||
strto
|
||||
@@ -331,7 +329,7 @@ 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 listed in UPGRADING.md. All four are fixed, so each of those files
|
||||
# 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.
|
||||
@@ -352,7 +350,7 @@ foreach(_test IN LISTS AKSL_TESTS AKSL_WILL_FAIL_TESTS AKSL_KNOWN_FAILING_TESTS)
|
||||
list(APPEND AKSL_TEST_TARGETS test_${_test})
|
||||
endforeach()
|
||||
|
||||
# Negative compile tests: the format attributes and AKERR_NOIGNORE.
|
||||
# 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
|
||||
|
||||
15
README.md
15
README.md
@@ -8,8 +8,7 @@ through [libakerror](https://source.starfort.tech/andrew/libakerror)'s
|
||||
and `errno`. It also provides data structures built on the same convention.
|
||||
|
||||
Every entry point returns `akerr_ErrorContext *` and is marked `AKERR_NOIGNORE`.
|
||||
Outstanding work is in [the issue tracker](https://source.starfort.tech/andrew/libakstdlib/issues).
|
||||
See `TODO.md` for where the library stands and what is deliberately not wrapped, and `UPGRADING.md` if you are
|
||||
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
|
||||
@@ -57,7 +56,7 @@ 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
|
||||
libakerror's pool, which is that library's decision to make; `TODO.md` §1.9
|
||||
records it. Until then: confine libakstdlib calls to one thread, or serialise
|
||||
them yourself.
|
||||
|
||||
@@ -101,7 +100,7 @@ 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
|
||||
in `TODO.md` §3 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
|
||||
@@ -226,10 +225,10 @@ 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 `UPGRADING.md`). Also marked `WILL_FAIL`. |
|
||||
| `AKSL_KNOWN_FAILING_TESTS` | Assert the *correct* behaviour of a confirmed defect (see `TODO.md` §2.1). Also marked `WILL_FAIL`. |
|
||||
|
||||
**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
|
||||
defects in `TODO.md` §2.1 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
|
||||
@@ -323,7 +322,7 @@ 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.
|
||||
workaround goes away when libakerror namespaces it upstream; see `TODO.md` §2.3.
|
||||
|
||||
CTest hides the output of a passing test, so `coverage_report` also writes
|
||||
`build-coverage/coverage-summary.txt` (the same text report) and
|
||||
@@ -380,7 +379,7 @@ lines are uncovered and each is uncovered on purpose:
|
||||
- **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.
|
||||
because anything reaches it. `TODO.md` §1.2 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,
|
||||
|
||||
372
TODO.md
372
TODO.md
@@ -1,16 +1,11 @@
|
||||
# Record
|
||||
# TODO
|
||||
|
||||
**Outstanding work is in the issue tracker, not in this file:**
|
||||
<https://source.starfort.tech/andrew/libakstdlib/issues>
|
||||
Working notes for `libakstdlib`. **Outstanding items only** — anything fixed comes
|
||||
out of this file and goes into `UPGRADING.md`, the tests, or a comment beside the
|
||||
code, whichever is the right place to be reminded of it.
|
||||
|
||||
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.
|
||||
Ordered by blast radius: what blocks other work first, what is merely wrong
|
||||
second, what is missing last.
|
||||
|
||||
## Where the library stands
|
||||
|
||||
@@ -24,61 +19,168 @@ it has been through grooming.
|
||||
| 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`.
|
||||
`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:
|
||||
## 1. Blocked on libakerror
|
||||
|
||||
| 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 |
|
||||
These are not fixable from inside this repository. Each one currently costs
|
||||
something here, and the cost is what makes them worth carrying.
|
||||
|
||||
## Uncovered lines that are uncoverable
|
||||
### 1.1 The error pool is a process-global array with no locking
|
||||
|
||||
Eight lines, and this is what they are, so the coverage listing does not read as an
|
||||
oversight.
|
||||
**This is what makes the library single-threaded**, and it is the largest open
|
||||
item by some distance.
|
||||
|
||||
**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.
|
||||
`AKERR_ARRAY_ERROR` is a fixed array in `deps/libakerror/src/error.c`, handed out
|
||||
by `akerr_next_error()` with no synchronisation of any kind. Every entry point in
|
||||
this library takes a slot from it on any failure path, so two threads raising
|
||||
errors concurrently can be handed the same slot and will corrupt each other's
|
||||
message, status and stack trace.
|
||||
|
||||
**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.
|
||||
**Consequence.** `README.md` says plainly that the library is not thread-safe.
|
||||
That is honest, and it is also a hard ceiling: §4's `pthread_*` and socket
|
||||
wrappers cannot be written until this is resolved, because a threading API
|
||||
nobody can call from a thread is not an API.
|
||||
|
||||
**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.
|
||||
**What closing it would touch.** libakerror's pool — either a mutex around
|
||||
`akerr_next_error`/`akerr_release_error`, or thread-local slot arrays, which
|
||||
would suit the bounded-preallocation style better and cost nothing on the
|
||||
single-threaded path. Then a TSan job in `.gitea/workflows/ci.yaml` and a
|
||||
concurrent smoke test here, and the warning in `README.md` comes out.
|
||||
|
||||
## `aksl_version_check()` ignores its `patch` argument
|
||||
### 1.2 `IGNORE()` logs a context and never releases it
|
||||
|
||||
`src/stdlib.c`, the `(void)patch`. **Correct for the current "same soname" rule** —
|
||||
`deps/libakerror/include/akerror.tmpl.h:308`. The macro assigns the context to
|
||||
`__akerr_last_ignored`, logs it, and stops. The slot is never returned to the
|
||||
pool, so every `IGNORE()` on a failing call leaks one — and after
|
||||
`AKERR_MAX_ARRAY_ERROR` of them the pool is exhausted and `ENSURE_ERROR_READY`
|
||||
calls `exit(1)`.
|
||||
|
||||
**Consequence here.** `aksl_tree_iterate`'s `CLEANUP` block (`src/stdlib.c`)
|
||||
cannot use `IGNORE()` to drop a queue-drain failure and open-codes the
|
||||
log-then-release by hand instead, with a comment saying why. It is four lines
|
||||
that should be one.
|
||||
|
||||
**What closing it would touch.** One `RELEASE_ERROR` in the macro; then delete
|
||||
the workaround here.
|
||||
|
||||
### 1.3 libakerror does not namespace its `coverage` target when embedded
|
||||
|
||||
`deps/libakerror/CMakeLists.txt:172` versus `:189` — it namespaces `mutation` and
|
||||
not `coverage`.
|
||||
|
||||
**Consequence here.** A `-DAKSL_COVERAGE=ON` top-level build fails to configure
|
||||
at all: *"another target with the same name already exists"*. `CMakeLists.txt`
|
||||
shadows `add_custom_target` for the duration of the `add_subdirectory()` call and
|
||||
renames the dependency's to `akerror_coverage`.
|
||||
|
||||
**What closing it would touch.** Apply the same
|
||||
`CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR` test upstream that the
|
||||
`mutation` target already has; then delete the shadow here, which sits directly
|
||||
above the `add_test` shadow and shares its comment.
|
||||
|
||||
### 1.4 libakerror installs no `akerrorConfigVersion.cmake`
|
||||
|
||||
**Consequence here.** `cmake/akstdlib.cmake.in` has to call
|
||||
`find_dependency(akerror)` with no version, because a request for one would be
|
||||
refused for want of a version file no matter what is installed. The 1.0.0 floor
|
||||
therefore rests on `akstdlib.pc`'s `Requires:` and the `#error` guard in
|
||||
`akstdlib.h`, neither of which covers a `find_package` consumer.
|
||||
|
||||
**What closing it would touch.** One `write_basic_package_version_file()` call
|
||||
upstream — libakstdlib already does this correctly and can be copied — then add
|
||||
the `1.0.0` floor to the `find_dependency` here.
|
||||
|
||||
---
|
||||
|
||||
## 2. Known gaps in what is already wrapped
|
||||
|
||||
### 2.1 A short transfer with neither EOF nor a stream error is untested
|
||||
|
||||
`src/stdlib.c`, the `FAIL_RETURN(e, AKERR_IO, "short read: ...")` in `aksl_fread`
|
||||
and its counterpart in `aksl_fwrite`. Two of the eight uncovered lines in the
|
||||
whole library.
|
||||
|
||||
The standard permits a short transfer with neither indicator set, so the branch
|
||||
is correct to have. Every way of actually producing one on Linux sets `feof` or
|
||||
`ferror` first, so nothing in the suite reaches it.
|
||||
|
||||
**Consequence.** Low: the code is a handful of lines and reviewed, but it is the
|
||||
only error path in the library that has never executed.
|
||||
|
||||
**What closing it would touch.** A `FILE *` over a custom stream — `fopencookie`
|
||||
on glibc, `funopen` on the BSDs — whose read function returns a short count
|
||||
without setting either flag. That is a platform-specific test helper in
|
||||
`tests/aksl_capture.h` guarded on the platform, which is why it has not been
|
||||
written yet rather than an oversight.
|
||||
|
||||
### 2.2 The string-buffer overflow guard is untestable
|
||||
|
||||
`src/collections.c`, 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. The other two uncovered lines.
|
||||
|
||||
**Consequence.** None known. It is there because doubling a capacity is a
|
||||
multiplication, and an unguarded one is how a growable buffer turns into a heap
|
||||
overflow.
|
||||
|
||||
**What closing it would touch.** Nothing worth doing. Recorded so the coverage
|
||||
listing does not read as an oversight.
|
||||
|
||||
### 2.3 `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.
|
||||
**Consequence.** None 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
|
||||
### 2.4 Surviving mutants worth turning into assertions
|
||||
|
||||
Recorded so nobody adds them thinking they were forgotten. **Each is a decision,
|
||||
and each can be revisited with an argument.**
|
||||
The mutation harness samples 260 of 1701 mutants and kills 72.3% of them. Most of
|
||||
the 72 survivors are equivalent mutants rather than missing tests — `README.md`
|
||||
has the full breakdown — but three clusters are real work:
|
||||
|
||||
- **`FINISH(e, true)` → `FINISH(e, false)`, 3 survivors.** An error swallowed
|
||||
instead of propagated out of an `ATTEMPT` block, and nothing notices. Each one
|
||||
is a call whose failure path is exercised but whose *propagation* is not: the
|
||||
test asserts the status the callee raised without checking it came from the
|
||||
callee rather than being re-raised locally. `tests/test_pool.c`'s origin
|
||||
assertions are the shape of the fix.
|
||||
- **`SUCCEED_RETURN` deleted, 7 survivors.** The function falls off the end and
|
||||
returns whatever is in the return register, which is NULL often enough to pass.
|
||||
These need an assertion on the *side effect* — the buffer that was filled, the
|
||||
node that was linked — rather than on the returned status.
|
||||
- **`FAIL_*` guards deleted, ~6 of the 14 in that group.** Each is an argument
|
||||
check nothing drives. The other 8 in the group are constant shifts that no test
|
||||
can catch, because the test names the same constant symbolically and moves with
|
||||
it.
|
||||
|
||||
Two survivors in this 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.
|
||||
|
||||
**What closing them would touch.** Only `tests/`. Raise the `--threshold` in
|
||||
`.gitea/workflows/ci.yaml` and `.githooks/pre-push` in step, as a ratchet.
|
||||
|
||||
### 2.5 Four uncovered `HANDLE(e, AKERR_ITERATOR_BREAK)` lines
|
||||
|
||||
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* — which is the pathological case
|
||||
the errno-fallback work removed. Left uncovered deliberately rather than pinned
|
||||
by a test that would have to manufacture it.
|
||||
|
||||
---
|
||||
|
||||
## 3. 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 |
|
||||
|---|---|
|
||||
@@ -89,23 +191,126 @@ and each can be revisited with an argument.**
|
||||
| `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.
|
||||
## 4. Not yet wrapped
|
||||
|
||||
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.
|
||||
Ordered by how much a caller of this library would miss them. §3.1 and §3.6 of
|
||||
the old numbering are done; what follows is what is left.
|
||||
|
||||
## Evidence from the first full consumer
|
||||
### 4.1 POSIX file and process API
|
||||
|
||||
The most likely next surface. Nothing here is blocked; it is simply not written.
|
||||
|
||||
**`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`
|
||||
|
||||
The short-read/short-write contract is the interesting part: `read(2)` returning
|
||||
fewer bytes than asked for is *normal* on a pipe or a socket and a failure on a
|
||||
regular file, so the wrapper needs the same transferred-count out-param
|
||||
`aksl_fread` has, and callers need to be told which case they are in.
|
||||
|
||||
**`sys/stat.h`**
|
||||
- [ ] `stat`, `fstat`, `lstat`, `fstatat`
|
||||
- [ ] `statvfs`, `fstatvfs`
|
||||
|
||||
**`dirent.h`**
|
||||
- [ ] `opendir`, `fdopendir`, `readdir`, `closedir`, `rewinddir`, `scandir`
|
||||
|
||||
`readdir(3)` returning NULL for both "end of directory" and "error, check errno"
|
||||
is the same conflation `aksl_fgetc` and `aksl_fgets` already untangle, and should
|
||||
be untangled the same way: AKERR_EOF for the end, the errno for the error.
|
||||
|
||||
**Process control**
|
||||
- [ ] `fork`, the `exec*` family, `waitpid`, `wait`
|
||||
- [ ] `posix_spawn`
|
||||
- [ ] `system`, `popen`, `pclose`
|
||||
- [ ] `getpid`, `getppid`, `getuid`, `geteuid`, `setuid`, `setgid`
|
||||
- [ ] `atexit`, `exit`, `_exit`, `abort` — mostly to give akerror a shutdown hook
|
||||
- [ ] `getenv`, `setenv`, `unsetenv`, `putenv`, `clearenv`
|
||||
|
||||
`fork` needs thinking about before it is wrapped: the error pool is inherited by
|
||||
the child, and any context live at the moment of the fork exists twice
|
||||
afterwards.
|
||||
|
||||
**`sys/mman.h`**
|
||||
- [ ] `mmap`, `munmap`, `mprotect`, `msync`, `madvise`
|
||||
|
||||
### 4.2 Time
|
||||
|
||||
- [ ] `time`, `clock_gettime`, `clock_getres`, `gettimeofday`
|
||||
- [ ] `localtime_r`, `gmtime_r`, `mktime`, `timegm`, `difftime`
|
||||
- [ ] `strftime`, `strptime`
|
||||
- [ ] `clock`, `times`
|
||||
|
||||
`strftime(3)` returns 0 for both "the output was empty" and "it did not fit",
|
||||
which is the bounded-write ambiguity `aksl_snprintf` already resolves.
|
||||
|
||||
### 4.3 Sorting, searching and the rest of `stdlib.h`
|
||||
|
||||
- [ ] `qsort`, `qsort_r`, `bsearch` — a comparator that fails currently has
|
||||
nowhere to put the error. An akerror-aware comparator signature, shaped
|
||||
like the `aksl_TreeCompareFunc` the tree functions already take, would be a
|
||||
genuine improvement over libc rather than a wrapper around it.
|
||||
- [ ] `abs`, `labs`, `llabs`, `div`, `ldiv`, `lldiv` — note that
|
||||
`abs(INT_MIN)` is undefined behaviour, which is exactly the kind of silent
|
||||
trap worth surfacing.
|
||||
- [ ] `rand`, `srand`, `random`, `srandom`, `getrandom`/`arc4random`
|
||||
|
||||
### 4.4 Larger surfaces
|
||||
|
||||
- [ ] **Sockets**: `socket`, `bind`, `listen`, `accept`, `connect`, the
|
||||
`send`/`recv` families, `shutdown`, `setsockopt`/`getsockopt`,
|
||||
`getaddrinfo`/`freeaddrinfo`/`gai_strerror`, `inet_ntop`/`inet_pton`.
|
||||
`getaddrinfo` is the interesting one: it has its own error space, neither
|
||||
errno nor akerror, which needs mapping into a registered status range.
|
||||
- [ ] **Multiplexing**: `select`, `poll`, `ppoll`, `epoll_*`
|
||||
- [ ] **Signals**: `sigaction`, `sigprocmask`, `sigemptyset`/`sigaddset`, `kill`,
|
||||
`raise`, `signalfd`. A handler cannot raise an akerror context — the pool is
|
||||
not async-signal-safe — so the wrapper covers installation and masking only,
|
||||
and that limit should be documented rather than discovered.
|
||||
- [ ] **Threads**: `pthread_create`/`join`/`detach`, `pthread_mutex_*`,
|
||||
`pthread_cond_*`, `pthread_rwlock_*`, `sem_*`. **Blocked on §1.1.**
|
||||
- [ ] **Dynamic loading**: `dlopen`, `dlsym`, `dlclose`, `dlerror`. `dlerror(3)`
|
||||
is the same "returns a string and clears itself" trap as `strerror`.
|
||||
- [ ] **Locale / wide chars**: `setlocale`, `mbstowcs`, `wcstombs`, `iconv_*`
|
||||
- [ ] **Math**: the `math.h` functions that set `errno` or raise FP exceptions
|
||||
(`sqrt`, `log`, `pow`, `acos`, …). Probably a dedicated `libakmath` rather
|
||||
than more surface here — the failure model is `fetestexcept`, not errno, and
|
||||
it does not resemble anything else in this library.
|
||||
|
||||
### 4.5 Data structures
|
||||
|
||||
The list and tree API is complete against what §3.6 asked for. What a second
|
||||
consumer might want next:
|
||||
|
||||
- [ ] A dynamic array / vector, on the same "caller owns the storage" terms as
|
||||
`aksl_HashMap`.
|
||||
- [ ] A hash map keyed on something other than a string — the current one copies
|
||||
keys into fixed slots, which is right for identifiers and wrong for
|
||||
anything large or binary.
|
||||
- [ ] Balanced insertion for the tree. It is a plain unbalanced BST and says so;
|
||||
sorted input gives a degenerate chain that `AKSL_TREE_MAX_DEPTH` then
|
||||
refuses. Bounded rather than dangerous, but a red-black or AVL variant is
|
||||
the honest fix if anyone inserts sorted data in earnest.
|
||||
|
||||
---
|
||||
|
||||
## 5. 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.**
|
||||
whole surface rather than a corner of it, and what it *could not* use is what
|
||||
prioritised the work above.
|
||||
|
||||
**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
|
||||
@@ -125,27 +330,28 @@ committed to it.
|
||||
|
||||
**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`.
|
||||
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's own `TODO.md` §1.9 formally bans 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. **`src/convert.c` can be deleted.**
|
||||
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
|
||||
**And the four confirmed-with-impact items are closed.** §2.2.4's unbounded
|
||||
`aksl_sprintf` is gone; §2.2.2's unchecked `aksl_fopen` 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.
|
||||
§2.2.6's sign-extended djb2 reads bytes unsigned; and §2.1.4's 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.
|
||||
**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 §4.1's `open`/`read`/`write` far higher than this one
|
||||
does. The next thing worth doing is porting akbasic onto this release and
|
||||
counting the calls again.
|
||||
|
||||
@@ -10,7 +10,7 @@ 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
|
||||
Everything below comes out of `TODO.md` sections 2.1 and 2.2 — the confirmed
|
||||
defects, all of which were reproduced against the 0.1.0 library before being
|
||||
fixed.
|
||||
|
||||
|
||||
@@ -26,8 +26,7 @@
|
||||
* takes a slot from it on any failure path. See README.md.
|
||||
*
|
||||
* @see README.md for the deviations from libc semantics, UPGRADING.md if you are
|
||||
* coming from 0.1.0. Outstanding work is in the issue tracker;
|
||||
* TODO.md is the record of where the library stands.
|
||||
* coming from 0.1.0, and TODO.md for what is still open.
|
||||
*/
|
||||
|
||||
#ifndef _AKSTDLIB_H_
|
||||
@@ -70,14 +69,12 @@
|
||||
*
|
||||
* It used to pull in stdlib.h and string.h as well, which nothing here needs and
|
||||
* which every consumer then got whether it wanted them or not. stddef.h in place
|
||||
* of stdlib.h is the same size_t at a fraction of the namespace.
|
||||
* of stdlib.h is the same size_t at a fraction of the namespace. TODO.md 2.2.16.
|
||||
*/
|
||||
#include <stdarg.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/statvfs.h>
|
||||
/* off_t, for the aksl_fseeko/aksl_ftello pair. POSIX, like aksl_realpath. */
|
||||
#include <sys/types.h>
|
||||
|
||||
@@ -88,7 +85,7 @@ extern "C" {
|
||||
/*
|
||||
* Restore the compile-time format/argument checking that callers otherwise lose
|
||||
* by going through a variadic wrapper: without it, printf("%d", "str") is caught
|
||||
* and aksl_printf(&n, "%d", "str") is not.
|
||||
* and aksl_printf(&n, "%d", "str") is not. TODO.md 2.2.5.
|
||||
*
|
||||
* tests/negative/format_mismatch.c is a compile that must fail, which is what
|
||||
* proves these are still attached.
|
||||
@@ -810,57 +807,6 @@ akerr_ErrorContext AKERR_NOIGNORE *aksl_strhash_djb2_str(const char *str, uint32
|
||||
|
||||
/** @} */
|
||||
|
||||
/**
|
||||
* @brief stat(2).
|
||||
* @param[in] pathname Path to inspect. Required.
|
||||
* @param[out] dest File metadata. Required.
|
||||
* @return NULL on success, an error context otherwise.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *aksl_stat(const char *pathname, struct stat *dest);
|
||||
|
||||
/**
|
||||
* @brief lstat(2), inspecting a symbolic link itself.
|
||||
* @param[in] pathname Path to inspect. Required.
|
||||
* @param[out] dest File metadata. Required.
|
||||
* @return NULL on success, an error context otherwise.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *aksl_lstat(const char *pathname, struct stat *dest);
|
||||
|
||||
/**
|
||||
* @brief fstat(2).
|
||||
* @param[in] fd Open file descriptor.
|
||||
* @param[out] dest File metadata. Required.
|
||||
* @return NULL on success, an error context otherwise.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *aksl_fstat(int fd, struct stat *dest);
|
||||
|
||||
/**
|
||||
* @brief fstatat(2).
|
||||
* @param[in] dirfd Directory descriptor, or AT_FDCWD.
|
||||
* @param[in] pathname Path to inspect. Required.
|
||||
* @param[out] dest File metadata. Required.
|
||||
* @param[in] flags libc fstatat flags, passed unchanged.
|
||||
* @return NULL on success, an error context otherwise.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *aksl_fstatat(int dirfd, const char *pathname, struct stat *dest, int flags);
|
||||
|
||||
/**
|
||||
* @brief statvfs(3).
|
||||
* @param[in] path Path on the filesystem. Required.
|
||||
* @param[out] dest Filesystem metadata. Required.
|
||||
* @return NULL on success, an error context otherwise.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *aksl_statvfs(const char *path, struct statvfs *dest);
|
||||
|
||||
/**
|
||||
* @brief fstatvfs(3).
|
||||
* @param[in] fd Open file descriptor.
|
||||
* @param[out] dest Filesystem metadata. Required.
|
||||
* @return NULL on success, an error context otherwise.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *aksl_fstatvfs(int fd, struct statvfs *dest);
|
||||
|
||||
|
||||
/* ====================================================================== */
|
||||
/** @name Streams: open, read, write, close
|
||||
*
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
* 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.
|
||||
* See UPGRADING.md.
|
||||
* TODO.md 2.2.1.
|
||||
*/
|
||||
#define AKSL_ERRNO_OR(__fallback) (errno != 0 ? errno : (__fallback))
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Data structures.
|
||||
* Data structures -- TODO.md section 3.6.
|
||||
*
|
||||
* Not libc wrappers. These are the parts of the list and tree API that were
|
||||
* visibly missing, plus the two structures the first real consumer had to write
|
||||
@@ -328,7 +328,7 @@ akerr_ErrorContext AKERR_NOIGNORE *aksl_list_iterate_reverse(aksl_ListNode *tail
|
||||
* aksl_list_append has to walk the whole list to find the tail, so building a
|
||||
* list of n nodes with it is O(n^2). That is fine for the handful of nodes the
|
||||
* bare-node API was written for and wrong for anything larger, which is what
|
||||
* the collections plan meant by "a head/tail-tracking container type so append is O(1)".
|
||||
* TODO.md 3.6 means by "a head/tail-tracking container type so append is O(1)".
|
||||
*
|
||||
* The container holds the length as well, so aksl_list_length stops being a
|
||||
* walk. It owns no memory -- the nodes are still the caller's -- so there is no
|
||||
@@ -440,7 +440,7 @@ akerr_ErrorContext AKERR_NOIGNORE *aksl_list_clear(aksl_List *list, aksl_FreeFun
|
||||
* through an out-param and may raise, like every other callback here.
|
||||
*
|
||||
* These are the functions that set and read aksl_TreeNode.parent, which was
|
||||
* declared and then never touched by anything in the library.
|
||||
* declared and then never touched by anything in the library (TODO.md 2.2.15).
|
||||
* aksl_tree_remove needs it: relinking a node's replacement means telling that
|
||||
* node's parent about it, and finding the parent by walking from the root again
|
||||
* would turn a removal into a second search.
|
||||
@@ -691,7 +691,7 @@ akerr_ErrorContext AKERR_NOIGNORE *aksl_tree_free_all(aksl_TreeNode **root, aksl
|
||||
/* ====================================================================== */
|
||||
|
||||
/*
|
||||
* FNV-1a, the other half of the collections plan's hash request. It differs from djb2 in
|
||||
* FNV-1a, the other half of TODO.md 3.6's hash request. It differs from djb2 in
|
||||
* XOR-then-multiply rather than multiply-then-add, which mixes the low bits
|
||||
* rather better -- worth having when the keys are short and share a prefix,
|
||||
* which is exactly what identifiers in a symbol table look like.
|
||||
@@ -730,7 +730,7 @@ akerr_ErrorContext AKERR_NOIGNORE *aksl_strhash_fnv1a_str(const char *str, uint3
|
||||
/*
|
||||
* Fixed-capacity, open-addressed, linear-probing, string-keyed.
|
||||
*
|
||||
* The shape is akbasic's src/symtab.c, which the collections plan called "worth lifting
|
||||
* The shape is akbasic's src/symtab.c, which TODO.md 3.6 says is "worth lifting
|
||||
* more or less verbatim": the caller supplies the slot array, the map refuses
|
||||
* rather than resizes when full, and the keys are copied into fixed-size slots
|
||||
* so the map owns them and a caller cannot outlive its own key strings.
|
||||
@@ -939,7 +939,7 @@ akerr_ErrorContext AKERR_NOIGNORE *aksl_hashmap_iterate(aksl_HashMap *map,
|
||||
*
|
||||
* The bounded formatting wrappers are the right answer when the destination is
|
||||
* a fixed buffer, and no answer at all when the output length is not known in
|
||||
* advance -- which is why the collections plan asked for this to "make the snprintf and
|
||||
* advance -- which is why TODO.md 3.6 asks for this to "make the snprintf and
|
||||
* strcat wrappers pleasant to use". Building a diagnostic, a serialised record
|
||||
* or a generated line means appending to something that grows.
|
||||
*
|
||||
|
||||
88
src/stat.c
88
src/stat.c
@@ -1,88 +0,0 @@
|
||||
/* sys/stat.h and sys/statvfs.h metadata wrappers. */
|
||||
#include <akstdlib.h>
|
||||
#include <errno.h>
|
||||
#include "aksl_internal.h"
|
||||
|
||||
/*
|
||||
* stat(2) follows pathname through any symbolic links and writes the target's
|
||||
* metadata into the caller-owned struct stat. A file that is gone, cannot be
|
||||
* searched, or lives below a non-directory component is reported as the errno
|
||||
* from libc rather than as a library-specific status.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *aksl_stat(const char *pathname, struct stat *dest)
|
||||
{
|
||||
PREPARE_ERROR(e);
|
||||
FAIL_ZERO_RETURN(e, pathname, AKERR_NULLPOINTER, "pathname=%p, dest=%p", (void *)pathname, (void *)dest);
|
||||
FAIL_ZERO_RETURN(e, dest, AKERR_NULLPOINTER, "pathname=%p, dest=%p", (void *)pathname, (void *)dest);
|
||||
errno = 0;
|
||||
FAIL_NONZERO_RETURN(e, stat(pathname, dest), AKSL_ERRNO_OR(AKERR_IO), "pathname=%s", pathname);
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
/*
|
||||
* lstat(2) is stat(2) without the final symbolic-link traversal. That is the
|
||||
* difference a caller needs when it is deciding whether a path is a link or
|
||||
* when the link's ownership and mode are the metadata of interest.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *aksl_lstat(const char *pathname, struct stat *dest)
|
||||
{
|
||||
PREPARE_ERROR(e);
|
||||
FAIL_ZERO_RETURN(e, pathname, AKERR_NULLPOINTER, "pathname=%p, dest=%p", (void *)pathname, (void *)dest);
|
||||
FAIL_ZERO_RETURN(e, dest, AKERR_NULLPOINTER, "pathname=%p, dest=%p", (void *)pathname, (void *)dest);
|
||||
errno = 0;
|
||||
FAIL_NONZERO_RETURN(e, lstat(pathname, dest), AKSL_ERRNO_OR(AKERR_IO), "pathname=%s", pathname);
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
/*
|
||||
* fstat(2) gets metadata from an already-open descriptor, so it has no path
|
||||
* lookup race and remains useful after the file has been renamed or unlinked.
|
||||
* A closed or otherwise invalid descriptor reports EBADF from libc.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *aksl_fstat(int fd, struct stat *dest)
|
||||
{
|
||||
PREPARE_ERROR(e); FAIL_ZERO_RETURN(e, dest, AKERR_NULLPOINTER, "fd=%d, dest=%p", fd, (void *)dest);
|
||||
errno = 0; FAIL_NONZERO_RETURN(e, fstat(fd, dest), AKSL_ERRNO_OR(AKERR_IO), "fd=%d", fd); SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
/*
|
||||
* fstatat(2) is the directory-descriptor form of stat(2). pathname is resolved
|
||||
* relative to dirfd unless it is absolute, and flags retain the libc choices
|
||||
* such as inspecting a link itself. Keeping those flags unchanged prevents this
|
||||
* wrapper from inventing a smaller policy than the POSIX call already exposes.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *aksl_fstatat(int dirfd, const char *pathname, struct stat *dest, int flags)
|
||||
{
|
||||
PREPARE_ERROR(e);
|
||||
FAIL_ZERO_RETURN(e, pathname, AKERR_NULLPOINTER, "dirfd=%d, pathname=%p, dest=%p", dirfd, (void *)pathname, (void *)dest);
|
||||
FAIL_ZERO_RETURN(e, dest, AKERR_NULLPOINTER, "dirfd=%d, pathname=%p, dest=%p", dirfd, (void *)pathname, (void *)dest);
|
||||
errno = 0;
|
||||
FAIL_NONZERO_RETURN(e, fstatat(dirfd, pathname, dest, flags), AKSL_ERRNO_OR(AKERR_IO), "dirfd=%d pathname=%s flags=%d", dirfd, pathname, flags);
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
/*
|
||||
* statvfs(3) writes information about the mounted filesystem containing path,
|
||||
* not merely the named file. The result includes the filesystem block sizes and
|
||||
* available space the caller needs before it decides whether an operation fits.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *aksl_statvfs(const char *path, struct statvfs *dest)
|
||||
{
|
||||
PREPARE_ERROR(e);
|
||||
FAIL_ZERO_RETURN(e, path, AKERR_NULLPOINTER, "path=%p, dest=%p", (void *)path, (void *)dest);
|
||||
FAIL_ZERO_RETURN(e, dest, AKERR_NULLPOINTER, "path=%p, dest=%p", (void *)path, (void *)dest);
|
||||
errno = 0;
|
||||
FAIL_NONZERO_RETURN(e, statvfs(path, dest), AKSL_ERRNO_OR(AKERR_IO), "path=%s", path);
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
/*
|
||||
* fstatvfs(3) is the descriptor form of statvfs(3). It asks the filesystem that
|
||||
* owns fd for the same capacity and flag information without resolving a path
|
||||
* again, and reports a bad descriptor through the errno libc supplies.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *aksl_fstatvfs(int fd, struct statvfs *dest)
|
||||
{
|
||||
PREPARE_ERROR(e); FAIL_ZERO_RETURN(e, dest, AKERR_NULLPOINTER, "fd=%d, dest=%p", fd, (void *)dest);
|
||||
errno = 0; FAIL_NONZERO_RETURN(e, fstatvfs(fd, dest), AKSL_ERRNO_OR(AKERR_IO), "fd=%d", fd); SUCCEED_RETURN(e);
|
||||
}
|
||||
44
src/stdlib.c
44
src/stdlib.c
@@ -99,7 +99,7 @@ akerr_ErrorContext AKERR_NOIGNORE *aksl_calloc(size_t nmemb, size_t size, void *
|
||||
* block valid*, so the near-universal `p = realloc(p, n)` leaks the original
|
||||
* every time it fails. Here the old pointer goes in and out through the same
|
||||
* out-param, and is left untouched -- still valid, still the caller's to free --
|
||||
* whenever an error is raised.
|
||||
* whenever an error is raised. TODO.md 3.1.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *aksl_realloc(void **ptr, size_t size)
|
||||
{
|
||||
@@ -196,7 +196,7 @@ akerr_ErrorContext AKERR_NOIGNORE *aksl_free(void *ptr)
|
||||
|
||||
/*
|
||||
* Frees and clears in one step, so the pointer cannot be used or freed twice.
|
||||
* aksl_free leaves the caller holding a dangling pointer, and
|
||||
* TODO.md 2.2.13 -- aksl_free leaves the caller holding a dangling pointer, and
|
||||
* "remember to NULL it afterwards" is exactly the discipline this library is
|
||||
* supposed to make unnecessary rather than merely possible.
|
||||
*/
|
||||
@@ -214,7 +214,7 @@ akerr_ErrorContext AKERR_NOIGNORE *aksl_freep(void **ptr)
|
||||
* memset(3) and memcpy(3) cannot fail. They return their destination pointer
|
||||
* unconditionally, so the old FAIL_ZERO_RETURN(e, memset(...), ...) and
|
||||
* (memcpy(...) == d) checks were dead code that read as though there were a
|
||||
* failure mode to catch. What is worth checking is the
|
||||
* failure mode to catch -- TODO.md 2.2.11. What is worth checking is the
|
||||
* arguments, which is all that is checked now.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *aksl_memset(void *s, int c, size_t n)
|
||||
@@ -289,7 +289,7 @@ akerr_ErrorContext AKERR_NOIGNORE *aksl_memchr(const void *s, int c, size_t n, v
|
||||
* pathname and mode are checked. fopen(NULL, ...) is undefined behaviour, and
|
||||
* this wrapper used to hand both straight through unexamined -- reachable from
|
||||
* user input in practice, which is why akbasic validates the filename itself
|
||||
* before calling DLOAD/DSAVE with a comment pointing at.
|
||||
* before calling DLOAD/DSAVE with a comment pointing at TODO.md 2.2.2.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *aksl_fopen(
|
||||
const char *pathname,
|
||||
@@ -312,7 +312,7 @@ akerr_ErrorContext AKERR_NOIGNORE *aksl_fopen(
|
||||
|
||||
/*
|
||||
* fread and fwrite both report how much they actually transferred, through a
|
||||
* required out-param. Three things were wrong with the old pair,
|
||||
* required out-param. Three things were wrong with the old pair (TODO.md 2.2.3),
|
||||
* and the count is the fix for the worst of them: a caller who got AKERR_EOF had
|
||||
* no way to find out how much data had arrived before the stream ran out, which
|
||||
* makes the EOF status almost useless for the partial-read case it exists to
|
||||
@@ -405,7 +405,7 @@ akerr_ErrorContext AKERR_NOIGNORE *aksl_fclose(FILE *stream)
|
||||
* Formatted output.
|
||||
*
|
||||
* The va_list forms below do the work and the variadic forms are thin wrappers
|
||||
* over them, which is both what the wrapper contract asked for -- so consumers can
|
||||
* over them, which is both what §3.1 of TODO.md asked for -- so consumers can
|
||||
* build their own variadic wrappers -- and what makes the va_end rule below
|
||||
* checkable in one place instead of three.
|
||||
*
|
||||
@@ -415,7 +415,7 @@ akerr_ErrorContext AKERR_NOIGNORE *aksl_fclose(FILE *stream)
|
||||
* one. Omitting it is undefined behaviour per the standard and leaks
|
||||
* register-save state on some ABIs. akbasic's text sink ran this UB on every
|
||||
* line of program output without anything visibly misbehaving, which is
|
||||
* exactly what made it worth fixing before something did.
|
||||
* exactly what made it worth fixing before something did. TODO.md 2.1.4.
|
||||
* - *count is written on every path. It used to be left holding vprintf's -1
|
||||
* after a failure, so a caller who read the length rather than the status got
|
||||
* a negative byte count out of a function that had already failed. It is now
|
||||
@@ -426,7 +426,7 @@ akerr_ErrorContext AKERR_NOIGNORE *aksl_fclose(FILE *stream)
|
||||
* aksl_sprintf is gone. It wrapped vsprintf, which cannot be bounded, and an
|
||||
* error-handling wrapper around an unbounded write is precisely the sharp edge
|
||||
* this library exists to remove. aksl_snprintf replaces it, and treats
|
||||
* truncation as the failure it is rather than as a short success.
|
||||
* truncation as the failure it is rather than as a short success. TODO.md 2.2.4.
|
||||
*/
|
||||
|
||||
akerr_ErrorContext AKERR_NOIGNORE *aksl_vprintf(int *count, const char *restrict format, va_list args)
|
||||
@@ -561,7 +561,7 @@ akerr_ErrorContext AKERR_NOIGNORE *aksl_vasprintf(int *count, char **dest, const
|
||||
* same arguments a few lines up, so the write cannot truncate. Guarding it
|
||||
* anyway would mean a failure branch nothing can reach and a free() beneath
|
||||
* it that nothing can execute -- dead code that reads as though there were a
|
||||
* failure mode to catch, which is exactly what was recorded
|
||||
* failure mode to catch, which is exactly what TODO.md 2.2.11 recorded
|
||||
* against the old aksl_memset and aksl_memcpy and what removing those was
|
||||
* for. The invariant is stated here instead, where it can be read.
|
||||
*/
|
||||
@@ -590,10 +590,10 @@ akerr_ErrorContext AKERR_NOIGNORE *aksl_asprintf(int *count, char **dest, const
|
||||
* atoi("99999999999999999999") handed back success and a wrapped value, because
|
||||
* atoi(3) has no error channel at all. A library whose entire value proposition
|
||||
* is turning silent libc failures into error contexts cannot ship that.
|
||||
* See UPGRADING.md.
|
||||
* TODO.md 2.1.5.
|
||||
*
|
||||
* The strto* wrappers below are the real implementation and the ato* wrappers
|
||||
* are three-line calls into them, which is what the wrapper contract asked for on its own
|
||||
* are three-line calls into them, which is what TODO.md 3.1 asked for on its own
|
||||
* account -- akbasic had to hand-write ~60 lines of exactly this (its
|
||||
* src/convert.c) because the library would not do it.
|
||||
*
|
||||
@@ -846,7 +846,7 @@ akerr_ErrorContext AKERR_NOIGNORE *aksl_atof(const char *nptr, double *dest)
|
||||
* realpath(3), with the destination buffer made expressible.
|
||||
*
|
||||
* The old two-argument form had three separate problems, all of them reachable
|
||||
* from ordinary use: resolved_path was never NULL-checked, so
|
||||
* from ordinary use (TODO.md 2.1.6): resolved_path was never NULL-checked, so
|
||||
* realpath(path, NULL) allocated a buffer the wrapper then discarded and leaked;
|
||||
* there was no way for a caller to say how big the buffer was, so everyone had
|
||||
* to know to supply PATH_MAX bytes; and the failure path formatted resolved_path
|
||||
@@ -904,7 +904,7 @@ akerr_ErrorContext AKERR_NOIGNORE *aksl_realpath_alloc(const char *restrict path
|
||||
* negative value: the hash differed from canonical djb2, and -- worse -- differed
|
||||
* between platforms depending on the signedness of char. Benign for the 7-bit
|
||||
* ASCII identifiers akbasic hashes, and quietly wrong for the first caller to
|
||||
* key a table on a filename or a UTF-8 string literal.
|
||||
* key a table on a filename or a UTF-8 string literal. TODO.md 2.2.6.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *aksl_strhash_djb2(const char *str, size_t len, uint32_t *hashval)
|
||||
{
|
||||
@@ -921,7 +921,7 @@ akerr_ErrorContext AKERR_NOIGNORE *aksl_strhash_djb2(const char *str, size_t len
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
/* The NUL-terminated convenience form the collections plan asked for. */
|
||||
/* The NUL-terminated convenience form TODO.md 3.6 asked for. */
|
||||
akerr_ErrorContext AKERR_NOIGNORE *aksl_strhash_djb2_str(const char *str, uint32_t *hashval)
|
||||
{
|
||||
PREPARE_ERROR(e);
|
||||
@@ -941,7 +941,7 @@ akerr_ErrorContext AKERR_NOIGNORE *aksl_list_append(aksl_ListNode *list, aksl_Li
|
||||
* Two separate walks, deliberately. The Floyd pass answers "is this list
|
||||
* finite"; it says nothing about where the tail is, because `slow` stops at
|
||||
* the midpoint. Conflating the two is what made this function truncate every
|
||||
* list of two or more nodes.
|
||||
* list of two or more nodes -- see TODO.md 2.1.1.
|
||||
*/
|
||||
while ( fast != NULL && fast->next != NULL ) {
|
||||
slow = slow->next;
|
||||
@@ -973,7 +973,7 @@ akerr_ErrorContext AKERR_NOIGNORE *aksl_list_append(aksl_ListNode *list, aksl_Li
|
||||
/*
|
||||
* `head` is required, not optional. Popping the head node used to leave the
|
||||
* caller's own head pointer aimed at a node that is no longer in the list, and
|
||||
* there was no way for the caller to learn the new one. Taking
|
||||
* there was no way for the caller to learn the new one -- TODO.md 2.2.12. Taking
|
||||
* the head by reference makes the correct call the only call that compiles.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *aksl_list_pop(aksl_ListNode **head, aksl_ListNode *node)
|
||||
@@ -998,7 +998,7 @@ akerr_ErrorContext AKERR_NOIGNORE *aksl_list_pop(aksl_ListNode **head, aksl_List
|
||||
/*
|
||||
* Zeroing initialisers. Every caller previously had to remember to memset a node
|
||||
* before its first use, because append and iterate both read next/prev -- and a
|
||||
* stack-allocated node that skipped it walked into garbage.
|
||||
* stack-allocated node that skipped it walked into garbage. TODO.md 2.2.14.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *aksl_list_node_init(aksl_ListNode *node, void *data)
|
||||
{
|
||||
@@ -1027,7 +1027,7 @@ akerr_ErrorContext AKERR_NOIGNORE *aksl_tree_node_init(aksl_TreeNode *node, void
|
||||
* aksl_ListNode: the walk needs to carry each node's depth alongside it, which is
|
||||
* how a cyclic tree is stopped without a visited-set. The caller's lalloc/lfree
|
||||
* allocate and release these -- which is what they were always documented to do
|
||||
* and never actually did.
|
||||
* and never actually did (TODO.md 2.2.8).
|
||||
*/
|
||||
typedef struct TreeQueueEntry {
|
||||
struct TreeQueueEntry *next;
|
||||
@@ -1165,12 +1165,12 @@ static akerr_ErrorContext AKERR_NOIGNORE *tree_bfs_walk(
|
||||
* unwinds every frame up to the public entry point, which swallows it exactly
|
||||
* once. In the old single-function form the frame that raised the break handled
|
||||
* it and returned success, the parent's PASS therefore saw nothing wrong, and
|
||||
* the walk carried on into the sibling subtree.
|
||||
* the walk carried on into the sibling subtree -- TODO.md 2.1.3.
|
||||
*
|
||||
* `path` is the chain of ancestors of `root` and `depth` its length. Checking
|
||||
* each node against its own ancestry turns a tree that loops back on itself from
|
||||
* infinite recursion into AKERR_CIRCULAR_REFERENCE, and the depth cap catches the
|
||||
* degenerate chain that is merely too deep to recurse over.
|
||||
* degenerate chain that is merely too deep to recurse over (TODO.md 2.2.7).
|
||||
*/
|
||||
static akerr_ErrorContext AKERR_NOIGNORE *tree_dfs_walk(
|
||||
aksl_TreeNode *root,
|
||||
@@ -1266,7 +1266,7 @@ akerr_ErrorContext AKERR_NOIGNORE *aksl_tree_iterate(
|
||||
* assume it. The switch used to have no default at all, so searchmode 99 --
|
||||
* and AKSL_TREE_SEARCH_VISIT, which the header documented but nothing
|
||||
* implemented -- fell straight through to success having visited nothing
|
||||
* See UPGRADING.md.
|
||||
* (TODO.md 2.2.9).
|
||||
*/
|
||||
switch ( searchmode ) {
|
||||
case AKSL_TREE_SEARCH_DFS_PREORDER:
|
||||
@@ -1328,7 +1328,7 @@ akerr_ErrorContext AKERR_NOIGNORE *aksl_list_iterate(aksl_ListNode *list, aksl_L
|
||||
/*
|
||||
* From `list`, not from `slow`. The Floyd pass above leaves `slow` at the
|
||||
* midpoint, and starting the visit there skipped the whole first half of the
|
||||
* list including the head.
|
||||
* list including the head -- see TODO.md 2.1.2.
|
||||
*/
|
||||
while ( node != NULL ) {
|
||||
ATTEMPT {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* stdio.h wrappers beyond fopen/fread/fwrite/fclose.
|
||||
* 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.
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
/*
|
||||
* string.h wrappers.
|
||||
* 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 the wrapper contract asks for.
|
||||
* rather than silently accepted" that TODO.md 3.1 asks for.
|
||||
*
|
||||
* Two conventions run through the whole file.
|
||||
*
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* NEGATIVE COMPILE TEST.
|
||||
* 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.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* NEGATIVE COMPILE TEST.
|
||||
* 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
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* List and tree additions -- src/collections.c.
|
||||
* List and tree additions -- src/collections.c, TODO.md section 3.6.
|
||||
*
|
||||
* The bare-node list functions, the tracked aksl_List container, and the binary
|
||||
* search tree. The hash map and string buffer have their own files.
|
||||
@@ -407,7 +407,7 @@ static akerr_ErrorContext AKERR_NOIGNORE *free_that_fails_once(void *ptr)
|
||||
* single walk. Only the first is kept and handed back; the rest have to be
|
||||
* released, or a walk over n nodes with a broken free would consume n pool slots
|
||||
* and exhaust the pool -- the failure mode the whole pool-accounting section of
|
||||
* the cross-cutting wrapper contract exists to catch.
|
||||
* TODO.md 1.9 exists to catch.
|
||||
*/
|
||||
static akerr_ErrorContext AKERR_NOIGNORE *free_that_always_fails(void *ptr)
|
||||
{
|
||||
@@ -800,7 +800,7 @@ static int test_tree_insert_orders_the_leaves(void)
|
||||
}
|
||||
|
||||
/*
|
||||
* aksl_TreeNode.parent was declared and never touched by
|
||||
* TODO.md 2.2.15: aksl_TreeNode.parent was declared and never touched by
|
||||
* anything in the library. These are the functions that set it, and
|
||||
* aksl_tree_remove is the one that needs it.
|
||||
*/
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/*
|
||||
* String -> number wrappers: aksl_atoi / atol / atoll / atof.
|
||||
*
|
||||
* Numeric conversion, complete. The cases that used to live in
|
||||
* 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
|
||||
@@ -95,7 +95,7 @@ static int test_trailing_junk_is_a_value_error(void)
|
||||
|
||||
/*
|
||||
* "0x10" through the ato* forms is base 10, so parsing stops at the 'x' and the
|
||||
* rest is trailing junk. The wrapper plan left this open; the answer is that 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.
|
||||
*/
|
||||
@@ -149,7 +149,7 @@ static int test_atoi_narrows_to_int_range(void)
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* The type boundaries round-trip exactly rather than nearly. */
|
||||
/* TODO.md 1.4: the type boundaries round-trip exactly rather than nearly. */
|
||||
static int test_type_boundaries_round_trip(void)
|
||||
{
|
||||
char buf[64];
|
||||
@@ -229,7 +229,7 @@ static int test_atof_converts_and_rejects_null(void)
|
||||
}
|
||||
|
||||
/*
|
||||
* The wrapper plan left "inf" and "nan" open. They are accepted, because strtod(3)
|
||||
* 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.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* Formatted-output wrappers: aksl_printf / aksl_fprintf / aksl_snprintf and
|
||||
* their va_list forms.
|
||||
*
|
||||
* Formatted output, complete. Each happy path asserts both halves of the
|
||||
* 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.
|
||||
@@ -11,7 +11,7 @@
|
||||
* points at the formatted-output wrapper under test and not at the stream
|
||||
* wrappers, which tests/test_stream.c covers.
|
||||
*
|
||||
* aksl_sprintf is gone and aksl_snprintf takes its place, so the
|
||||
* 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.
|
||||
*/
|
||||
@@ -139,7 +139,7 @@ static int test_fprintf_writes_to_stream(void)
|
||||
/*
|
||||
* 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:
|
||||
* The wrapper plan recorded the negative count as a contract gap, and this is the
|
||||
* 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)
|
||||
@@ -270,7 +270,7 @@ static int test_asprintf_allocates_to_fit(void)
|
||||
}
|
||||
|
||||
/*
|
||||
* The va_list forms are what the variadic ones are built on, and the wrapper contract
|
||||
* 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.
|
||||
*/
|
||||
@@ -304,7 +304,7 @@ static int test_va_list_forms_are_usable_from_outside(void)
|
||||
}
|
||||
|
||||
/*
|
||||
* Regression cover for the missing va_end. Nothing here can
|
||||
* 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.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* The fixed-capacity hash map and FNV-1a -- src/collections.c.
|
||||
* 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,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Linked list.
|
||||
* Linked list -- TODO.md section 1.7, complete.
|
||||
*
|
||||
* 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
|
||||
@@ -59,7 +59,7 @@ static akerr_ErrorContext AKERR_NOIGNORE *record_visit(aksl_ListNode *node, void
|
||||
/* ---------------------------------------------------------------------- */
|
||||
|
||||
/*
|
||||
* every caller used to have to remember to memset a node before
|
||||
* 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)
|
||||
@@ -103,7 +103,7 @@ static int test_append_single_node(void)
|
||||
* 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.
|
||||
* chain "n0 -> n4" and silently dropped n1, n2 and n3. TODO.md 2.1.1.
|
||||
*/
|
||||
static int test_append_builds_the_whole_chain(void)
|
||||
{
|
||||
@@ -218,7 +218,7 @@ static int test_append_detects_cycle_below_the_head(void)
|
||||
}
|
||||
|
||||
/*
|
||||
* The wrapper plan asked for the aliasing contract to be defined. It is refusal:
|
||||
* 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.
|
||||
@@ -281,7 +281,7 @@ static int test_iterate_single_node(void)
|
||||
* 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.
|
||||
* never passed to the callback at all. TODO.md 2.1.2.
|
||||
*/
|
||||
static int test_iterate_visits_every_node_from_the_head(void)
|
||||
{
|
||||
@@ -470,7 +470,7 @@ static int test_pop_middle_node(void)
|
||||
}
|
||||
|
||||
/*
|
||||
* popping the head used to leave the caller's own head pointer
|
||||
* 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.
|
||||
*/
|
||||
@@ -572,7 +572,7 @@ static int test_pop_then_iterate(void)
|
||||
}
|
||||
|
||||
/*
|
||||
* The wrapper plan's pool-accounting case. AKSL_RUN already asserts that each test
|
||||
* 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
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
/*
|
||||
* Memory wrappers, complete, plus the additions from
|
||||
* Memory wrappers -- TODO.md section 1.1, now complete, plus the additions from
|
||||
* section 3.1.
|
||||
*
|
||||
* The three cases the wrapper plan left open are all pinned here: malloc(0) is AKERR_VALUE
|
||||
* 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
|
||||
@@ -32,7 +32,7 @@ static int test_malloc_rejects_null_destination(void)
|
||||
}
|
||||
|
||||
/*
|
||||
* The wrapper plan asked for this contract to be pinned down. malloc(0) is allowed to
|
||||
* 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
|
||||
@@ -296,7 +296,7 @@ static int test_memcpy_zero_length_is_noop(void)
|
||||
}
|
||||
|
||||
/*
|
||||
* The wrapper plan's open question, decided: overlap is AKERR_VALUE. memcpy(3) calls
|
||||
* 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.
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
/*
|
||||
* aksl_realpath and aksl_realpath_alloc.
|
||||
* 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: the wrapper's own error path
|
||||
* 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
|
||||
@@ -122,7 +122,7 @@ static int test_rejects_null_arguments(void)
|
||||
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_realpath(NULL, resolved, sizeof(resolved)),
|
||||
AKERR_NULLPOINTER, "path=");
|
||||
/*
|
||||
* this used to be unchecked, and realpath(path, NULL)
|
||||
* 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),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Cross-cutting properties every wrapper has to have.
|
||||
* 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:
|
||||
*
|
||||
|
||||
@@ -1,105 +0,0 @@
|
||||
/* File metadata wrapper tests. */
|
||||
#include "aksl_capture.h"
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
|
||||
static int test_stat_success_and_fstat(void)
|
||||
{
|
||||
char path[AKSL_TMP_MAX];
|
||||
int fd = -1;
|
||||
struct stat path_dest;
|
||||
struct stat fd_dest;
|
||||
struct statvfs vfs_dest;
|
||||
/* A real file makes the mode and four-byte size observable to stat(2). */
|
||||
AKSL_CHECK(aksl_temp_file(path, sizeof(path)) == 0);
|
||||
fd = open(path, O_RDWR);
|
||||
AKSL_CHECK(fd >= 0);
|
||||
AKSL_CHECK(write(fd, "stat", 4) == 4);
|
||||
AKSL_CHECK_OK(aksl_stat(path, &path_dest));
|
||||
AKSL_CHECK(S_ISREG(path_dest.st_mode));
|
||||
AKSL_CHECK(path_dest.st_size == 4);
|
||||
|
||||
/* fstat(2) addresses the same opened inode, not a second pathname lookup. */
|
||||
AKSL_CHECK_OK(aksl_fstat(fd, &fd_dest));
|
||||
AKSL_CHECK(fd_dest.st_ino == path_dest.st_ino);
|
||||
|
||||
/* fstatvfs(3) describes the backing filesystem and has a usable block size. */
|
||||
AKSL_CHECK_OK(aksl_fstatvfs(fd, &vfs_dest));
|
||||
AKSL_CHECK(vfs_dest.f_frsize != 0);
|
||||
AKSL_CHECK(close(fd) == 0);
|
||||
|
||||
/* A descriptor that was just closed must surface libc's EBADF. */
|
||||
AKSL_CHECK_STATUS(aksl_fstat(fd, &fd_dest), EBADF);
|
||||
AKSL_CHECK(unlink(path) == 0);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int test_stat_paths_and_fstatat(void)
|
||||
{
|
||||
char path[AKSL_TMP_MAX];
|
||||
char child[AKSL_TMP_MAX * 2];
|
||||
char linkpath[AKSL_TMP_MAX * 2];
|
||||
struct stat dest;
|
||||
struct stat ldest;
|
||||
struct statvfs vdest;
|
||||
/* Missing paths and a regular file used as a directory preserve errno. */
|
||||
AKSL_CHECK_STATUS(aksl_stat("/nonexistent/aksl/stat", &dest), ENOENT);
|
||||
/* The temporary path is a regular file, so adding a child tests ENOTDIR. */
|
||||
AKSL_CHECK(aksl_temp_file(path, sizeof(path)) == 0);
|
||||
AKSL_CHECK(snprintf(child, sizeof(child), "%s/child", path) < (int)sizeof(child));
|
||||
AKSL_CHECK_STATUS(aksl_stat(child, &dest), ENOTDIR);
|
||||
AKSL_CHECK(snprintf(linkpath, sizeof(linkpath), "%s.link", path) < (int)sizeof(linkpath));
|
||||
AKSL_CHECK(symlink(path, linkpath) == 0);
|
||||
|
||||
/* stat follows the link while lstat reports the link object itself. */
|
||||
AKSL_CHECK_OK(aksl_stat(linkpath, &dest));
|
||||
AKSL_CHECK_OK(aksl_lstat(linkpath, &ldest));
|
||||
AKSL_CHECK(S_ISREG(dest.st_mode));
|
||||
AKSL_CHECK(S_ISLNK(ldest.st_mode));
|
||||
|
||||
/* AT_FDCWD makes fstatat resolve this path from the current directory. */
|
||||
AKSL_CHECK_OK(aksl_fstatat(AT_FDCWD, path, &dest, 0));
|
||||
|
||||
/* Invalid flags must replace stale errno with the EINVAL libc reports. */
|
||||
errno = E2BIG;
|
||||
AKSL_CHECK_STATUS(aksl_fstatat(AT_FDCWD, path, &dest, 0x40000000), EINVAL);
|
||||
AKSL_CHECK(errno == EINVAL);
|
||||
|
||||
/* statvfs reports the filesystem containing the current directory. */
|
||||
AKSL_CHECK_OK(aksl_statvfs(".", &vdest));
|
||||
AKSL_CHECK(vdest.f_frsize != 0);
|
||||
AKSL_CHECK(unlink(linkpath) == 0);
|
||||
AKSL_CHECK(unlink(path) == 0);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int test_stat_null_arguments(void)
|
||||
{
|
||||
char path[AKSL_TMP_MAX];
|
||||
struct stat dest;
|
||||
struct statvfs vdest;
|
||||
/* Create a valid path so each failure below isolates a NULL argument. */
|
||||
AKSL_CHECK(aksl_temp_file(path, sizeof(path)) == 0);
|
||||
/* Every wrapper refuses either missing caller-owned input or output storage. */
|
||||
AKSL_CHECK_STATUS(aksl_stat(NULL, &dest), AKERR_NULLPOINTER);
|
||||
AKSL_CHECK_STATUS(aksl_stat(path, NULL), AKERR_NULLPOINTER);
|
||||
AKSL_CHECK_STATUS(aksl_lstat(NULL, &dest), AKERR_NULLPOINTER);
|
||||
AKSL_CHECK_STATUS(aksl_lstat(path, NULL), AKERR_NULLPOINTER);
|
||||
AKSL_CHECK_STATUS(aksl_fstat(0, NULL), AKERR_NULLPOINTER);
|
||||
AKSL_CHECK_STATUS(aksl_fstatat(AT_FDCWD, NULL, &dest, 0), AKERR_NULLPOINTER);
|
||||
AKSL_CHECK_STATUS(aksl_fstatat(AT_FDCWD, path, NULL, 0), AKERR_NULLPOINTER);
|
||||
AKSL_CHECK_STATUS(aksl_statvfs(NULL, &vdest), AKERR_NULLPOINTER);
|
||||
AKSL_CHECK_STATUS(aksl_statvfs(path, NULL), AKERR_NULLPOINTER);
|
||||
AKSL_CHECK_STATUS(aksl_fstatvfs(0, NULL), AKERR_NULLPOINTER);
|
||||
AKSL_CHECK(unlink(path) == 0);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
int failures = 0;
|
||||
AKSL_RUN(failures, test_stat_success_and_fstat);
|
||||
AKSL_RUN(failures, test_stat_paths_and_fstatat);
|
||||
AKSL_RUN(failures, test_stat_null_arguments);
|
||||
AKSL_REPORT(failures);
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* The growable string buffer -- src/collections.c.
|
||||
* 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.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/*
|
||||
* Stream wrappers: aksl_fopen / aksl_fread / aksl_fwrite / aksl_fclose.
|
||||
*
|
||||
* Stream wrappers, complete. The happy paths, the round trip, every
|
||||
* 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
|
||||
@@ -81,7 +81,7 @@ static int test_fopen_rejects_null_arguments(void)
|
||||
|
||||
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_fopen(path, "r", NULL),
|
||||
AKERR_NULLPOINTER, "fp=");
|
||||
/* both of these used to go straight through to fopen(3). */
|
||||
/* 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),
|
||||
@@ -144,7 +144,7 @@ static int test_fread_short_read_is_eof_and_reports_the_count(void)
|
||||
AKSL_CHECK_OK(aksl_fopen(path, "r", &fp));
|
||||
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_fread(buf, 1, sizeof(buf), fp, &moved),
|
||||
AKERR_EOF, "EOF");
|
||||
/* this is what the caller could not previously find out. */
|
||||
/* 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));
|
||||
|
||||
@@ -160,7 +160,7 @@ static int test_fread_short_read_is_eof_and_reports_the_count(void)
|
||||
*
|
||||
* 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
|
||||
* keeps AKERR_IO as the fallback for the case where the stream
|
||||
* (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)
|
||||
@@ -194,7 +194,7 @@ static int test_fread_rejects_null_arguments(void)
|
||||
|
||||
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_fread(buf, 1, sizeof(buf), NULL, &moved),
|
||||
AKERR_NULLPOINTER, "fp=");
|
||||
/* ptr was never checked in either direction. */
|
||||
/* 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),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Stream wrappers beyond open/read/write/close -- src/stream.c.
|
||||
* 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
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* aksl_strhash_djb2.
|
||||
* 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
|
||||
@@ -7,7 +7,7 @@
|
||||
*
|
||||
* 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 the sign-extension defect down.
|
||||
* vector is the one that pins TODO.md 2.2.6 down.
|
||||
*/
|
||||
|
||||
#include "aksl_capture.h"
|
||||
@@ -47,7 +47,7 @@ static int test_known_answer_vectors(void)
|
||||
}
|
||||
|
||||
/*
|
||||
* Pinned. The cursor is an unsigned char * now, so bytes at or
|
||||
* 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
|
||||
@@ -68,7 +68,7 @@ static int test_high_bit_bytes_are_unsigned(void)
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* The NUL-terminated convenience form agrees with the length one. */
|
||||
/* 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";
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* String wrappers -- src/string.c.
|
||||
* 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
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* The strto* family.
|
||||
* 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
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Tree traversal.
|
||||
* Tree traversal -- TODO.md section 1.8, complete.
|
||||
*
|
||||
* 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
|
||||
@@ -183,7 +183,7 @@ static int test_dfs_is_an_alias_for_preorder(void)
|
||||
|
||||
/*
|
||||
* BFS was AKERR_NOT_IMPLEMENTED, and the lalloc/lfree parameters that existed to
|
||||
* serve it were defaulted and then never called. See UPGRADING.md.
|
||||
* 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)
|
||||
@@ -249,7 +249,7 @@ static akerr_ErrorContext AKERR_NOIGNORE *counting_free(void *ptr)
|
||||
}
|
||||
|
||||
/*
|
||||
* The wrapper plan asked that "custom lalloc/lfree are actually invoked -- currently they are
|
||||
* 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.
|
||||
@@ -382,7 +382,7 @@ static int test_degenerate_chains(void)
|
||||
}
|
||||
|
||||
/*
|
||||
* A chain deeper than the recursion can take. It used to
|
||||
* 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
|
||||
@@ -488,7 +488,7 @@ static int test_cyclic_tree_is_caught(void)
|
||||
* 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.
|
||||
* 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
|
||||
@@ -579,7 +579,7 @@ static int test_null_arguments(void)
|
||||
}
|
||||
|
||||
/*
|
||||
* the switch had no default, so an unrecognised mode -- and
|
||||
* 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.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Version reporting.
|
||||
* 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:
|
||||
|
||||
Reference in New Issue
Block a user