8 Commits

Author SHA1 Message Date
cff2a64575 Recount the consumer calls against this release
All checks were successful
libakstdlib CI Build / cmake_build (push) Successful in 2m52s
libakstdlib CI Build / sanitizers (push) Successful in 2m59s
libakstdlib CI Build / coverage (push) Successful in 2m45s
libakstdlib CI Build / mutation_test (push) Successful in 12m0s
akbasic's src/ ported onto 0.2.0 calls this library 301 times and raw libc
13 -- 4.1% bypassed, against 86.4% on the same tree before the port and
92.2% at the first count. The port builds clean at -Wall -Wextra, passes
112/112 of akbasic's ctest suite and is ASan+UBSan-clean.

The method was never written down and the figure was not reproducible.
scripts/consumer_calls.py is that method, and reproducing it turned up two
corrections: the old 116 was 117 by its own table's arithmetic, and 119 by
a complete count -- the table had no row for strncmp or memmove.

Nothing was blocked by a missing wrapper. 272 of 285 sites converted; the
13 that did not are blocked by wrapper shape, and are filed as #32-#38.

Say plainly what the number does not cover: akbasic makes 0 calls into
list, tree, hash map and string buffer combined, so the recount is
evidence about the string, memory and format surface and about nothing
else.

Refs #26

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-03 12:53:19 -04:00
2b79aca103 Merge pull request 'Wrap file metadata calls' (#31) from 9 into main
All checks were successful
libakstdlib CI Build / sanitizers (push) Successful in 2m51s
libakstdlib CI Build / cmake_build (push) Successful in 2m55s
libakstdlib CI Build / coverage (push) Successful in 2m52s
libakstdlib CI Build / mutation_test (push) Successful in 12m4s
Reviewed-on: #31
2026-08-03 11:05:43 -04:00
15e9104d9e Test file metadata failure paths
All checks were successful
libakstdlib CI Build / coverage (push) Successful in 2m51s
libakstdlib CI Build / cmake_build (push) Successful in 2m59s
libakstdlib CI Build / sanitizers (push) Successful in 3m4s
libakstdlib CI Build / mutation_test (push) Successful in 12m14s
Co-authored-by: Andrew Kesterson <andrew@aklabs.net>
2026-08-03 10:43:47 -04:00
01034fc668 Document file metadata wrapper behavior
All checks were successful
libakstdlib CI Build / coverage (push) Successful in 2m44s
libakstdlib CI Build / cmake_build (push) Successful in 2m56s
libakstdlib CI Build / sanitizers (push) Successful in 3m1s
libakstdlib CI Build / mutation_test (push) Successful in 12m58s
Co-authored-by: Andrew Kesterson <andrew@aklabs.net>
2026-08-03 09:02:19 -04:00
8c94231167 Wrap file metadata calls
All checks were successful
libakstdlib CI Build / cmake_build (push) Successful in 3m2s
libakstdlib CI Build / sanitizers (push) Successful in 3m3s
libakstdlib CI Build / coverage (push) Successful in 3m1s
libakstdlib CI Build / mutation_test (push) Successful in 12m46s
Co-authored-by: Andrew Kesterson <andrew@aklabs.net>
2026-08-03 07:34:16 -04:00
be725f8cf2 Merge pull request 'Document libc wrapper contract' (#30) from 9 into main
All checks were successful
libakstdlib CI Build / cmake_build (push) Successful in 2m53s
libakstdlib CI Build / sanitizers (push) Successful in 3m3s
libakstdlib CI Build / coverage (push) Successful in 2m45s
libakstdlib CI Build / mutation_test (push) Successful in 11m59s
Reviewed-on: #30
2026-08-03 06:53:05 -04:00
58f426abce Document libc wrapper contract
All checks were successful
libakstdlib CI Build / coverage (push) Successful in 2m47s
libakstdlib CI Build / sanitizers (push) Successful in 2m57s
libakstdlib CI Build / cmake_build (push) Successful in 3m15s
libakstdlib CI Build / mutation_test (push) Successful in 12m35s
Co-authored-by: Andrew Kesterson <andrew@aklabs.net>
2026-08-03 06:38:21 -04:00
55d986c631 Drop the TODO.md section numbers from 26 files
All checks were successful
libakstdlib CI Build / cmake_build (push) Successful in 5m53s
libakstdlib CI Build / sanitizers (push) Successful in 2m55s
libakstdlib CI Build / coverage (push) Successful in 2m48s
libakstdlib CI Build / mutation_test (push) Successful in 12m39s
Eighty-five comments cited section numbers -- 1.1, 2.2.6, 3.6 -- from a
numbering the file had already abandoned before the move to the tracker. They
label completed work, so the pointer was the only wrong part.

The citation is removed and the sentence kept, which is what issue #27
recommended: these are labels, not references, and a label carrying a
version-dependent pointer goes stale again at the next reorganisation. Where a
pointer earns its place it names what actually holds the content now --
UPGRADING.md for the confirmed defects, libakerror #15 for the target
namespacing, issue #7 for the mutation survivors.

README.md and akstdlib.h sent readers to TODO.md for 'what is still open';
they name the tracker.

Verified: cmake --build build && ctest --test-dir build, 19/19.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
2026-08-02 22:01:27 -04:00
32 changed files with 831 additions and 108 deletions

View File

@@ -21,7 +21,7 @@ jobs:
# different libakerrors depending on where you built: the install went to # different libakerrors depending on where you built: the install went to
# /usr/local, while the build below is top-level and so compiles # /usr/local, while the build below is top-level and so compiles
# deps/libakerror at the pinned commit via add_subdirectory -- the # deps/libakerror at the pinned commit via add_subdirectory -- the
# installed one was never actually linked against. TODO.md 2.3. # installed one was never actually linked against.
# #
# The submodule wins. It is the version this repository pins, tests and # 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 # 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 # new surface being argument validation whose mutants are frequently
# equivalent. `errno = 0` deleted from a wrapper whose libc call always # equivalent. `errno = 0` deleted from a wrapper whose libc call always
# sets errno cannot be distinguished by any test that could be written. # sets errno cannot be distinguished by any test that could be written.
# The survivors worth acting on are named in TODO.md; raise the gate as # The survivors worth acting on are named in issue #7; raise the gate as
# they become assertions. # they become assertions.
- name: mutation testing - name: mutation testing
run: | run: |

View File

@@ -76,6 +76,10 @@ return convention and the `PREPARE_ERROR` / `FAIL_*` / `SUCCEED_RETURN` pattern.
Four conventions hold across the whole library, and a new wrapper that breaks one Four conventions hold across the whole library, and a new wrapper that breaks one
of them is wrong even if it compiles and passes: 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". - **A NULL out-param is a caller error**, not "don't care".
- **Finding nothing is success** -- searching functions write NULL or zero and - **Finding nothing is success** -- searching functions write NULL or zero and
return NULL. return NULL.
@@ -93,6 +97,11 @@ 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 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. 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 ## Testing Guidelines
Add a new test by creating `tests/test_mything.c` and adding `mything` to the Add a new test by creating `tests/test_mything.c` and adding `mything` to the

View File

@@ -5,7 +5,7 @@ cmake_minimum_required(VERSION 3.10)
# akstdlibConfigVersion.cmake. Nothing else should spell a version number. # 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 # 0.2.0, and the minor bump is an ABI break on purpose. Fixing the confirmed
# defects in TODO.md 2.1 changed documented behaviour and, in five places, # defects listed in UPGRADING.md changed documented behaviour and, in five places,
# signatures: # signatures:
# #
# aksl_realpath takes the destination's length; aksl_realpath_alloc is new # 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() endif()
# Warnings. The only warning in the tree when these went on was the unused # Warnings. The only warning in the tree when these went on was the unused
# `queue` parameter of aksl_tree_iterate, which TODO.md 2.2.8 had already # `queue` parameter of aksl_tree_iterate, which UPGRADING.md had already
# recorded as a dead parameter -- so the cost of turning them on was one # 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. # 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 # 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 # 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 # src/stdlib.c passes at least one argument regardless -- see the note in
# TODO.md 2.3 -- so the code is pedantic-clean; it is the expansion that is not. # libakerror issue #15 -- so the code is pedantic-clean; it is the expansion that is not.
if(CMAKE_C_COMPILER_ID MATCHES "^(GNU|Clang|AppleClang)$") if(CMAKE_C_COMPILER_ID MATCHES "^(GNU|Clang|AppleClang)$")
add_compile_options(-Wall -Wextra) add_compile_options(-Wall -Wextra)
# CI turns this on. Locally it is off, because a warning that stops the build # CI turns this on. Locally it is off, because a warning that stops the build
@@ -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 # 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 # dropping it: its coverage script drives its own instrumented build tree, so
# `cmake --build build-coverage --target akerror_coverage` still does the right # `cmake --build build-coverage --target akerror_coverage` still does the right
# thing. Remove this once the dependency namespaces it upstream -- see TODO.md. # thing. Remove this once the dependency namespaces it upstream -- libakerror issue #15.
function(add_custom_target _name) function(add_custom_target _name)
if(AKSL_SUPPRESS_ADD_TEST AND _name STREQUAL "coverage") if(AKSL_SUPPRESS_ADD_TEST AND _name STREQUAL "coverage")
_add_custom_target(akerror_coverage ${ARGN}) _add_custom_target(akerror_coverage ${ARGN})
@@ -207,6 +207,7 @@ add_library(akstdlib SHARED
src/stdlib.c src/stdlib.c
src/string.c src/string.c
src/stream.c src/stream.c
src/stat.c
src/collections.c src/collections.c
) )
@@ -317,6 +318,7 @@ set(AKSL_TESTS
strbuf strbuf
stream stream
streamio streamio
stat
strhash strhash
string string
strto strto
@@ -329,7 +331,7 @@ set(AKSL_WILL_FAIL_TESTS
# Empty, and that is the news. It held four entries -- convert_strict, # 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 # list_append_chain, list_iterate_head and tree_iterate_break -- one for each
# confirmed defect in TODO.md 2.1. All four are fixed, so each of those files # confirmed defect listed in UPGRADING.md. All four are fixed, so each of those files
# was folded back into the test for the thing it was testing (tests/ # 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 # 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. # has to keep passing rather than merely keep failing visibly.
@@ -350,7 +352,7 @@ foreach(_test IN LISTS AKSL_TESTS AKSL_WILL_FAIL_TESTS AKSL_KNOWN_FAILING_TESTS)
list(APPEND AKSL_TEST_TARGETS test_${_test}) list(APPEND AKSL_TEST_TARGETS test_${_test})
endforeach() endforeach()
# Negative compile tests -- TODO.md 1.3 and 1.9. # Negative compile tests: the format attributes and AKERR_NOIGNORE.
# #
# Two properties of this library are enforced by the compiler and by nothing # 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 # else: AKERR_NOIGNORE makes discarding a returned error context an error, and

View File

@@ -8,7 +8,8 @@ through [libakerror](https://source.starfort.tech/andrew/libakerror)'s
and `errno`. It also provides data structures built on the same convention. and `errno`. It also provides data structures built on the same convention.
Every entry point returns `akerr_ErrorContext *` and is marked `AKERR_NOIGNORE`. Every entry point returns `akerr_ErrorContext *` and is marked `AKERR_NOIGNORE`.
See `TODO.md` for the current state of the library and `UPGRADING.md` if you are 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
coming from 0.1.0, which this release breaks. coming from 0.1.0, which this release breaks.
## What it wraps ## What it wraps
@@ -180,8 +181,9 @@ would notice.
## Testing ## Testing
There are four harnesses. The first three take seconds; the fourth takes about There are five harnesses. The first three take seconds and the fifth is instant;
half an hour. the fourth takes about half an hour. The fifth is the only one that measures
something outside this repository.
### 1. The test suite ### 1. The test suite
@@ -468,6 +470,41 @@ right-leaning tree would have blown the stack the depth cap exists to protect),
and `aksl_tree_remove` on an empty tree, which without its guard dereferences and `aksl_tree_remove` on an empty tree, which without its guard dereferences
NULL. Both are in the suite now — which is what the harness is for. NULL. Both are in the suite now — which is what the harness is for.
### 5. Consumer adoption
Coverage says the tests reach the code and mutation testing says they would
notice it breaking. Neither says anybody *wanted* the code. That question only has
an external answer, so there is a harness for it too:
```sh
scripts/consumer_calls.py ../akbasic/src # the ratio
scripts/consumer_calls.py ../akbasic/src --detail --per-file # where it comes from
scripts/consumer_calls.py ../akbasic/src --baseline 301/13 # against a past count
```
It counts, across a consumer's source directory, how often that consumer calls
this library against how often it reaches past it to the libc function this
library wraps. Calls to libc functions **not** wrapped here — `isdigit`, `exit`,
`qsort` — score on neither side; the question is how often an *available* wrapper
gets bypassed. Comments and string literals are stripped before counting, and the
wrapped-libc set is read out of `include/akstdlib.h` rather than hardcoded, so a
recount after a release measures the surface that release actually shipped.
A wrapper nobody calls either does not fit or is not discoverable, and both are
this library's problem rather than the consumer's. `TODO.md` carries the standing
figures and what they did and did not justify.
Two warnings, both learned the hard way and both printed by `--baseline`:
- **A rate is only comparable between two counts of the same tree.** If the
consumer grew between them, compare the percentage and say which commit each
number came from. Comparing the totals across a tree that tripled in size is how
a real improvement gets reported as a regression, or the reverse.
- **One consumer's ratio is evidence, not a plan.** A consumer that draws
everything from fixed pools will never call the allocator however good the
allocator is. Weight the result by what the consumer is, and get a second
consumer before treating any ranking as settled.
## The pre-push hook ## The pre-push hook
`.githooks/pre-push` runs the fast harnesses — the default build and the `.githooks/pre-push` runs the fast harnesses — the default build and the

126
TODO.md
View File

@@ -22,6 +22,7 @@ it has been through grooming.
| Function coverage | 100% (154/154) | | Function coverage | 100% (154/154) |
| Doxygen | 100% of 154, gated — `cmake --build build --target docs` fails on an undocumented function, parameter or return | | Doxygen | 100% of 154, gated — `cmake --build build --target docs` fails on an undocumented function, parameter or return |
| Mutation score | 72.3% (188/260 sampled from 1701), gated at 65 | | Mutation score | 72.3% (188/260 sampled from 1701), gated at 65 |
| Consumer adoption | akbasic ported onto 0.2.0 calls this library 301 times and raw libc 13 — **4.1% bypassed**, from 92.2% at first count. Ungated, and one consumer only |
The six confirmed defects that used to head this file are fixed and 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, `AKSL_KNOWN_FAILING_TESTS` is empty. What they were, and what changed as a result,
@@ -102,15 +103,16 @@ are fixed: the right child's `depth + 1` in the depth-first walk, and
## Evidence from the first full consumer ## Evidence from the first full consumer
`akbasic` (`source.starfort.tech/andrew/akbasic`) is a ~6,300-line C interpreter `akbasic` (`source.starfort.tech/andrew/akbasic`) is a C interpreter built on this
built on this library and `libakerror`. It was the first consumer to exercise the library and `libakerror` — ~6,300 lines of `src/` when it was first measured,
whole surface rather than a corner of it, **and what it could not use is what 20,169 now. It was the first consumer to exercise the whole surface rather than a
prioritised everything that has been built since.** corner of it, **and what it could not use is what prioritised everything that has
been built since.**
**The number that started it.** Across `src/`, akbasic made **10 calls into this **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 library and 119 to raw libc** — a library whose value proposition is "turn silent
libc failures into error contexts", bypassed 92% of the time by the consumer most libc failures into error contexts", bypassed **92%** of the time by the consumer
committed to it. most committed to it.
| Raw libc it had to use | Count | Now available as | | Raw libc it had to use | Count | Now available as |
|---|---|---| |---|---|---|
@@ -121,8 +123,17 @@ committed to it.
| `strncpy` | 15 | `aksl_strncpy` | | `strncpy` | 15 | `aksl_strncpy` |
| `strtoll` / `strtod` | 2 | `aksl_strtoll` / `aksl_strtod` | | `strtoll` / `strtod` | 2 | `aksl_strtoll` / `aksl_strtod` |
| `fgets` | 2 | `aksl_fgets` | | `fgets` | 2 | `aksl_fgets` |
| `strncmp` | 1 | `aksl_strncmp` |
| `memmove` | 1 | `aksl_memmove` |
| `strstr` | 1 | `aksl_strstr` | | `strstr` | 1 | `aksl_strstr` |
Two corrections to that figure, both found by rebuilding it. It used to read 116;
the table it sat above summed to 117 and had no row for `strncmp` or `memmove`.
119 is what `scripts/consumer_calls.py` returns against akbasic `4e188b2`, and it
is the number everything below compares to. **The method is now a script rather
than a paragraph, because recovering it afterwards cost more than writing it down
would have.**
**All four things the port had to write for itself now exist here.** **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 1. **A strict `strtoll`/`strtod` wrapper** (`akbasic/src/convert.c`, ~60 lines). The
@@ -144,8 +155,99 @@ committed to it.
the sign-extended djb2 reads bytes unsigned; and the missing `va_end` — which 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. 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 ### The recount, against this release
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 akbasic's `src/` was ported onto 0.2.0 and counted again (#26). The port builds
does**, so one consumer's count is evidence, not a plan. Re-counting against this clean at `-Wall -Wextra`, passes **112/112** of akbasic's ctest suite, and is
release is #26. ASan+UBSan-clean.
| | libakstdlib | raw libc | bypassed |
|---|---|---|---|
| Baseline — akbasic `4e188b2`, 5,679 lines of `src/` | 10 | 119 | **92.2%** |
| Before the port — akbasic `330d731`, 20,169 lines | 45 | 285 | **86.4%** |
| **After the port — same tree** | **301** | **13** | **4.1%** |
**Read the third row against the second, not the first.** The tree tripled between
the baseline and the port, so 10/119 and 45/285 are counts of two different
programs; only 86.4% → 4.1% is a like-for-like measurement. The 45 in the middle
row is worth its own note — akbasic had already adopted `aksl_f*` across
`runtime_disk.c` on its own, without anybody counting.
**Nothing was blocked by a missing wrapper.** Every libc call akbasic makes had an
`aksl_*` counterpart. 272 of the 285 sites converted; the 13 that did not are
blocked by wrapper *shape*, and they are the useful output:
| Why it could not be used | Sites | Where |
|---|---|---|
| No error channel to route into — the enclosing function returns `bool` or `void`, or is a `bsearch` comparator whose signature libc fixes | 8 | `structtype.c` `word_is`, `environment.c` `akbasic_environment_is_waiting_for` (public API), `scanner.c` `is_at_end` and `peek_next`, `verbs.c` `verb_compare`, `format.c` `overflow`, `sink_akgl.c` `scroll` (×2) |
| Truncation is the answer, not the error | 4 | `format.c`, `structtype.c`, `runtime_struct.c`, `renumber.c` |
| Short-circuit is memory-safety-load-bearing and the compare cannot be hoisted past the NULL arm guarding it | 1 | `runtime_trap.c` |
The truncation four are worth spelling out, because they are a contract decision
rather than an accident. `PRINT USING "###"; 1E300` prints `***` today: the render
truncates, the truncated text has no `.`, and the formatter takes its overflow
path on exactly that. Through `aksl_snprintf` it raises `AKERR_OUTOFBOUNDS` out of
the interpreter instead. Two more are truncation-tolerant renderers that print
what fits and stop, and the fourth uses `snprintf`'s return to raise akbasic's
own `AKBASIC_ERR_BOUNDS` with its own message.
### What the recount found, and where it went
Every one of the 13 blocked sites came back to wrapper *shape* rather than a
missing wrapper, and the same seven shapes recurred across ten independent
conversion passes. They are filed, not listed here:
| Finding | Filed as |
|---|---|
| `aksl_snprintf`'s `count` out-param is required, so ~20 sites carry an `int written` that is written and never read. Raised by all ten passes. `-Wall -Wextra` cannot see it — `&written` is a use | #32 |
| No equality comparison. All 43 comparison sites flatten the three-way `int` to `== 0`; not one wants an ordering, and five now need a sentinel whose *initial value is load-bearing* | #33 |
| No truncating format and no length query, which is the whole of the truncation-four above | #34 |
| `aksl_hashmap_*` carries one payload, which is the only reason `akbasic/src/symtab.c` still exists | #35 |
| `aksl_fgets` signals end of input by raising, so a read loop cannot be a condition | #36 |
| A caller cannot add its own context to a wrapper's error, so it raises and discards instead — eight lines where there were two | #37 |
| No form a `bool` predicate or a `void` function can call, which is 8 of the 13 blocked sites. Carries the `ctype.h` question and the infallible-`memset` question with it | #38 |
`#14` already covered the `bsearch` comparator, and the port confirmed it from the
consumer side.
**The one thing the wrappers did better than the libc they replaced** is worth
recording next to the complaints: `aksl_fgets`'s `len_out` **deleted** two `strlen`
calls rather than converting them, and is more correct than what it replaced for a
line containing an embedded NUL. It is the only one of 272 conversions that
produced less code than it started with.
### Still true, and still the reason one count is not a plan
akbasic uses no allocator, no lists and no trees, drawing everything from fixed
pools by design, and porting it did not change that. Of the 301 calls it now
makes:
| Area | Calls | |
|---|---|---|
| Strings | 122 | 40.5% |
| Memory | 69 | 22.9% |
| Formatted output | 59 | 19.6% |
| Streams | 38 | 12.6% |
| String → number | 12 | 4.0% |
| Hashing | 1 | 0.3% |
| **Collections** | **0** | **0%** |
**Four fifths of the evidence is strings, memory and formatting.** The collections
work — list, tree, hash map, string buffer, `src/collections.c` and the largest
single body of code in this library — has **not one consumer call site**, and the
single hashing call next to it is `aksl_strhash_djb2` feeding a hash table akbasic
wrote for itself. **A consumer that does allocate would weight the
`open`/`read`/`write` work far higher than this one does**, so this remains
evidence and not a plan.
**The number to distrust is not the 4.1%; it is the 0%.** A recount that moves
92% to 4% on one consumer says the string, memory and format wrappers fit the
consumer that asked for them. It says nothing at all about the half of the library
that consumer never calls, and it cannot, however many times it is run. What would
say something is a second consumer with different shape — one that allocates.
`akbasic/src/symtab.c` is the sharpest instance. It is the hand-rolled fixed-capacity
string-keyed hash table `aksl_hashmap_*` was generalised from, it survived the port
untouched, and the reason turned out to be one field rather than a design
disagreement — everything else about the two already lines up. #35 has it, and it
is the first collections work with a consumer actually waiting for it.

View File

@@ -26,7 +26,8 @@
* takes a slot from it on any failure path. See README.md. * 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 * @see README.md for the deviations from libc semantics, UPGRADING.md if you are
* coming from 0.1.0, and TODO.md for what is still open. * coming from 0.1.0. Outstanding work is in the issue tracker;
* TODO.md is the record of where the library stands.
*/ */
#ifndef _AKSTDLIB_H_ #ifndef _AKSTDLIB_H_
@@ -69,12 +70,15 @@
* *
* It used to pull in stdlib.h and string.h as well, which nothing here needs and * 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 * 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. TODO.md 2.2.16. * of stdlib.h is the same size_t at a fraction of the namespace.
*/ */
#include <stdarg.h> #include <stdarg.h>
#include <stddef.h> #include <stddef.h>
#include <stdint.h> #include <stdint.h>
#include <stdio.h> #include <stdio.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/statvfs.h>
/* off_t, for the aksl_fseeko/aksl_ftello pair. POSIX, like aksl_realpath. */ /* off_t, for the aksl_fseeko/aksl_ftello pair. POSIX, like aksl_realpath. */
#include <sys/types.h> #include <sys/types.h>
@@ -85,7 +89,7 @@ extern "C" {
/* /*
* Restore the compile-time format/argument checking that callers otherwise lose * Restore the compile-time format/argument checking that callers otherwise lose
* by going through a variadic wrapper: without it, printf("%d", "str") is caught * by going through a variadic wrapper: without it, printf("%d", "str") is caught
* and aksl_printf(&n, "%d", "str") is not. TODO.md 2.2.5. * and aksl_printf(&n, "%d", "str") is not.
* *
* tests/negative/format_mismatch.c is a compile that must fail, which is what * tests/negative/format_mismatch.c is a compile that must fail, which is what
* proves these are still attached. * proves these are still attached.
@@ -807,6 +811,88 @@ akerr_ErrorContext AKERR_NOIGNORE *aksl_strhash_djb2_str(const char *str, uint32
/** @} */ /** @} */
/* ====================================================================== */
/** @name File and filesystem metadata
*
* The stat family reports into caller-owned POSIX structs rather than a smaller
* library-defined copy: the platform owns the fields, and a wrapper should not
* discard a field merely because this library does not currently use it. libc
* failures retain their errno value as the status, so callers can distinguish
* absent paths from inaccessible ones without parsing an error message.
* @{
*/
/* ====================================================================== */
/**
* @brief stat(2).
* @param[in] pathname Path to inspect. Required.
* @param[out] dest File metadata. Required.
* @throws AKERR_NULLPOINTER If pathname or dest is NULL.
* @throws AKERR_IO If stat(2) failed and left errno at 0.
* @throws (errno) The errno stat(2) set, reported directly as the status.
* @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.
* @throws AKERR_NULLPOINTER If pathname or dest is NULL.
* @throws AKERR_IO If lstat(2) failed and left errno at 0.
* @throws (errno) The errno lstat(2) set, reported directly as the status.
* @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.
* @throws AKERR_NULLPOINTER If dest is NULL.
* @throws AKERR_IO If fstat(2) failed and left errno at 0.
* @throws (errno) The errno fstat(2) set, reported directly as the status.
* @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.
* @throws AKERR_NULLPOINTER If pathname or dest is NULL.
* @throws AKERR_IO If fstatat(2) failed and left errno at 0.
* @throws (errno) The errno fstatat(2) set, reported directly as the status.
* @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.
* @throws AKERR_NULLPOINTER If path or dest is NULL.
* @throws AKERR_IO If statvfs(3) failed and left errno at 0.
* @throws (errno) The errno statvfs(3) set, reported directly as the status.
* @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.
* @throws AKERR_NULLPOINTER If dest is NULL.
* @throws AKERR_IO If fstatvfs(3) failed and left errno at 0.
* @throws (errno) The errno fstatvfs(3) set, reported directly as the status.
* @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 /** @name Streams: open, read, write, close
* *

245
scripts/consumer_calls.py Executable file
View File

@@ -0,0 +1,245 @@
#!/usr/bin/env python3
"""
Consumer adoption harness for libakstdlib.
Answers one question about a consumer's source tree: how often does it call
this library, and how often does it reach past this library to the libc
function this library wraps?
The ratio is the only external evidence there is about whether the surface that
got built is the surface anyone wanted. A wrapper nobody calls is a wrapper that
either does not fit or is not discoverable, and both are the library's problem.
The number this reports is only worth something if it is reproducible, which is
why this is a script and not a paragraph. TODO.md's first consumer figure was
recorded without one, and recovering the method afterwards cost more than
writing it down would have.
WHAT COUNTS
* The corpus is every *.c and *.h directly under the given directory. It does
not recurse: a consumer's src/ is the thing being measured, not its vendored
dependencies, and those are usually a subdirectory.
* Comments and string/character literals are stripped before anything is
counted, so a function named in prose or inside a format string does not
score. This matters more than it sounds -- "strlen" appears in doc comments
throughout a codebase that has been thinking about strlen.
* A call site is IDENT immediately followed by '(', where IDENT is not
preceded by an identifier character. Declarations are not distinguished from
calls; a consumer that declares a function named for a libc entry point will
over-count by one per declaration, which is visible in --detail.
* A library call is any IDENT matching ^aksl_.
* A bypass is any IDENT naming a libc function this library wraps. That set is
read out of include/akstdlib.h rather than hardcoded, so it grows when the
library grows and a recount after a release measures the surface that
release actually shipped.
* libc functions this library does NOT wrap -- isdigit, exit, qsort -- score
on neither side. The question is how often a consumer bypasses an available
wrapper, not how much libc it uses. Adding a wrapper for something and
having it ignored is a finding; a consumer calling exit() is not.
WHAT IT CANNOT TELL YOU
One consumer's ratio is evidence, not a plan. A consumer that draws
everything from fixed pools will never call the allocator no matter how good
the allocator is, and will weight the string wrappers accordingly. Weight the
result by what the consumer is, and get a second consumer before treating any
ranking as settled.
Usage:
scripts/consumer_calls.py DIR [options]
DIR consumer source directory to measure, e.g.
../akbasic/src
--header PATH akstdlib.h to read the wrapped-libc set from
(default: include/akstdlib.h beside this script's repo)
--detail list the per-function breakdown on both sides
--per-file list per-file counts, worst bypass ratio first
--baseline A/B compare against a previous count, e.g. --baseline 10/119
--json emit the whole result as JSON instead of text
"""
import argparse
import json
import os
import re
import sys
from collections import Counter
# Wrapper families that are this library's own constructs rather than a libc
# function under a new name. aksl_list_append has no libc counterpart, so
# "append" must not become a name a consumer can be scored for bypassing.
LIBRARY_ONLY_PREFIXES = ("hashmap", "list", "tree", "strbuf", "version",
"strhash")
# Library-only names whose first underscore-separated word is shared with a real
# libc entry point, so a prefix rule cannot separate them. aksl_realpath wraps
# realpath(3) and must score; aksl_realpath_alloc is this library's own.
LIBRARY_ONLY_NAMES = frozenset(("freep", "realpath_alloc"))
CALL = re.compile(r"(?<![A-Za-z0-9_])([A-Za-z_][A-Za-z0-9_]*)\s*\(")
def wrapped_libc(header):
"""The set of libc names this library wraps, read out of the header."""
with open(header, encoding="utf-8", errors="replace") as handle:
text = handle.read()
names = set()
for match in re.finditer(r"\baksl_([a-z0-9_]+)\s*\(", text):
name = match.group(1)
if name.split("_")[0] in LIBRARY_ONLY_PREFIXES:
continue
if name in LIBRARY_ONLY_NAMES:
continue
names.add(name)
return names
def strip_c(src):
"""Remove comments and string/char literals, preserving everything else."""
out = []
i, end = 0, len(src)
while i < end:
char = src[i]
if char == "/" and i + 1 < end and src[i + 1] == "/":
while i < end and src[i] != "\n":
i += 1
elif char == "/" and i + 1 < end and src[i + 1] == "*":
i += 2
while i + 1 < end and not (src[i] == "*" and src[i + 1] == "/"):
i += 1
i += 2
elif char in ('"', "'"):
quote = char
i += 1
while i < end and src[i] != quote:
if src[i] == "\\":
i += 1
i += 1
i += 1
out.append(" ")
else:
out.append(char)
i += 1
return "".join(out)
def measure(srcdir, libc):
"""Count library and bypass call sites across one directory."""
library, bypass, per_file = Counter(), Counter(), {}
names = sorted(name for name in os.listdir(srcdir)
if name.endswith((".c", ".h")))
for name in names:
with open(os.path.join(srcdir, name), encoding="utf-8",
errors="replace") as handle:
text = strip_c(handle.read())
here_lib = here_raw = 0
for match in CALL.finditer(text):
ident = match.group(1)
if ident.startswith("aksl_"):
library[ident] += 1
here_lib += 1
elif ident in libc:
bypass[ident] += 1
here_raw += 1
if here_lib or here_raw:
per_file[name] = (here_lib, here_raw)
return library, bypass, per_file
def rate(bypassed, total):
return 100.0 * bypassed / total if total else 0.0
def main():
parser = argparse.ArgumentParser(add_help=True)
parser.add_argument("srcdir")
parser.add_argument("--header")
parser.add_argument("--detail", action="store_true")
parser.add_argument("--per-file", action="store_true")
parser.add_argument("--baseline")
parser.add_argument("--json", action="store_true")
args = parser.parse_args()
header = args.header or os.path.join(
os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
"include", "akstdlib.h")
if not os.path.isfile(header):
sys.stderr.write(f"error: no such header: {header}\n")
return 2
if not os.path.isdir(args.srcdir):
sys.stderr.write(f"error: no such directory: {args.srcdir}\n")
return 2
libc = wrapped_libc(header)
library, bypass, per_file = measure(args.srcdir, libc)
lib_total, raw_total = sum(library.values()), sum(bypass.values())
total = lib_total + raw_total
if args.json:
print(json.dumps({
"source": os.path.abspath(args.srcdir),
"header": os.path.abspath(header),
"wrapped_libc_names": len(libc),
"library_calls": lib_total,
"bypass_calls": raw_total,
"bypass_pct": round(rate(raw_total, total), 1),
"library_breakdown": dict(library.most_common()),
"bypass_breakdown": dict(bypass.most_common()),
"per_file": {k: {"library": v[0], "bypass": v[1]}
for k, v in per_file.items()},
}, indent=2))
return 0
print(f"consumer : {os.path.abspath(args.srcdir)}")
print(f"measured against: {os.path.abspath(header)} "
f"({len(libc)} wrapped libc names)")
print()
print(f"libakstdlib calls : {lib_total}")
print(f"bypassed to libc : {raw_total}")
print(f"bypass rate : {rate(raw_total, total):.1f}% "
f"({raw_total}/{total})")
if args.baseline:
try:
was_lib, was_raw = (int(part) for part in args.baseline.split("/"))
except ValueError:
sys.stderr.write("error: --baseline wants LIBRARY/BYPASS, "
"e.g. 10/119\n")
return 2
was_total = was_lib + was_raw
print()
print(f"baseline : {was_lib} / {was_raw} "
f"({rate(was_raw, was_total):.1f}% bypass)")
print(f"change : {lib_total - was_lib:+d} library, "
f"{raw_total - was_raw:+d} bypass, "
f"{rate(raw_total, total) - rate(was_raw, was_total):+.1f} pt")
print()
print("A bypass rate is only comparable between two counts of the same")
print("tree. If the consumer grew between them, compare the rate and")
print("not the totals -- and say which tree each number came from.")
if args.detail:
print("\nbypassed to libc")
for name, count in bypass.most_common():
print(f" {name:<22}{count}")
print("\ncalls into libakstdlib")
for name, count in library.most_common():
print(f" {name:<22}{count}")
if args.per_file:
print("\nper file (library, bypass), worst bypass first")
order = sorted(per_file.items(), key=lambda kv: (-kv[1][1], kv[0]))
for name, (lib, raw) in order:
print(f" {name:<32}{lib:>5}{raw:>6}")
return 0
if __name__ == "__main__":
sys.exit(main())

View File

@@ -18,7 +18,7 @@
* the context still holds a pool slot: an error that is invisible and leaks at * 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, * 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. * and every wrapped call clears errno first so the value read back is its own.
* TODO.md 2.2.1. * See UPGRADING.md.
*/ */
#define AKSL_ERRNO_OR(__fallback) (errno != 0 ? errno : (__fallback)) #define AKSL_ERRNO_OR(__fallback) (errno != 0 ? errno : (__fallback))

View File

@@ -1,5 +1,5 @@
/* /*
* Data structures -- TODO.md section 3.6. * Data structures.
* *
* Not libc wrappers. These are the parts of the list and tree API that were * 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 * 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 * 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 * 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 * bare-node API was written for and wrong for anything larger, which is what
* TODO.md 3.6 means by "a head/tail-tracking container type so append is O(1)". * the collections plan meant 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 * 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 * 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. * 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 * These are the functions that set and read aksl_TreeNode.parent, which was
* declared and then never touched by anything in the library (TODO.md 2.2.15). * declared and then never touched by anything in the library.
* aksl_tree_remove needs it: relinking a node's replacement means telling that * 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 * node's parent about it, and finding the parent by walking from the root again
* would turn a removal into a second search. * 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 TODO.md 3.6's hash request. It differs from djb2 in * FNV-1a, the other half of the collections plan's hash request. It differs from djb2 in
* XOR-then-multiply rather than multiply-then-add, which mixes the low bits * 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, * rather better -- worth having when the keys are short and share a prefix,
* which is exactly what identifiers in a symbol table look like. * 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. * Fixed-capacity, open-addressed, linear-probing, string-keyed.
* *
* The shape is akbasic's src/symtab.c, which TODO.md 3.6 says is "worth lifting * The shape is akbasic's src/symtab.c, which the collections plan called "worth lifting
* more or less verbatim": the caller supplies the slot array, the map refuses * 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 * 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. * 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 * 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 * a fixed buffer, and no answer at all when the output length is not known in
* advance -- which is why TODO.md 3.6 asks for this to "make the snprintf and * advance -- which is why the collections plan asked for this to "make the snprintf and
* strcat wrappers pleasant to use". Building a diagnostic, a serialised record * strcat wrappers pleasant to use". Building a diagnostic, a serialised record
* or a generated line means appending to something that grows. * or a generated line means appending to something that grows.
* *

108
src/stat.c Normal file
View File

@@ -0,0 +1,108 @@
/*
* sys/stat.h and sys/statvfs.h metadata wrappers.
*
* These calls make information about a file or filesystem available only by a
* return value that must be checked alongside a caller-owned POSIX struct. The
* wrappers put failure in the return value, where AKERR_NOIGNORE prevents it
* from being silently dropped, while preserving the errno that distinguishes
* missing paths, inaccessible paths, and invalid descriptors. errno is cleared
* immediately before each libc call, so a broken libc that reports failure
* without setting errno is still an AKERR_IO failure rather than status 0.
*/
#include <akstdlib.h>
#include <errno.h>
#include <sys/stat.h>
#include <sys/statvfs.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=0x%x", 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);
}

View File

@@ -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 * 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 * 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 -- * out-param, and is left untouched -- still valid, still the caller's to free --
* whenever an error is raised. TODO.md 3.1. * whenever an error is raised.
*/ */
akerr_ErrorContext AKERR_NOIGNORE *aksl_realloc(void **ptr, size_t size) 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. * Frees and clears in one step, so the pointer cannot be used or freed twice.
* TODO.md 2.2.13 -- aksl_free leaves the caller holding a dangling pointer, and * aksl_free leaves the caller holding a dangling pointer, and
* "remember to NULL it afterwards" is exactly the discipline this library is * "remember to NULL it afterwards" is exactly the discipline this library is
* supposed to make unnecessary rather than merely possible. * 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 * memset(3) and memcpy(3) cannot fail. They return their destination pointer
* unconditionally, so the old FAIL_ZERO_RETURN(e, memset(...), ...) and * unconditionally, so the old FAIL_ZERO_RETURN(e, memset(...), ...) and
* (memcpy(...) == d) checks were dead code that read as though there were a * (memcpy(...) == d) checks were dead code that read as though there were a
* failure mode to catch -- TODO.md 2.2.11. What is worth checking is the * failure mode to catch. What is worth checking is the
* arguments, which is all that is checked now. * arguments, which is all that is checked now.
*/ */
akerr_ErrorContext AKERR_NOIGNORE *aksl_memset(void *s, int c, size_t n) 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 * pathname and mode are checked. fopen(NULL, ...) is undefined behaviour, and
* this wrapper used to hand both straight through unexamined -- reachable from * this wrapper used to hand both straight through unexamined -- reachable from
* user input in practice, which is why akbasic validates the filename itself * user input in practice, which is why akbasic validates the filename itself
* before calling DLOAD/DSAVE with a comment pointing at TODO.md 2.2.2. * before calling DLOAD/DSAVE with a comment pointing at.
*/ */
akerr_ErrorContext AKERR_NOIGNORE *aksl_fopen( akerr_ErrorContext AKERR_NOIGNORE *aksl_fopen(
const char *pathname, 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 * fread and fwrite both report how much they actually transferred, through a
* required out-param. Three things were wrong with the old pair (TODO.md 2.2.3), * required out-param. Three things were wrong with the old pair,
* and the count is the fix for the worst of them: a caller who got AKERR_EOF had * 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 * 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 * 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. * Formatted output.
* *
* The va_list forms below do the work and the variadic forms are thin wrappers * The va_list forms below do the work and the variadic forms are thin wrappers
* over them, which is both what §3.1 of TODO.md asked for -- so consumers can * over them, which is both what the wrapper contract asked for -- so consumers can
* build their own variadic wrappers -- and what makes the va_end rule below * build their own variadic wrappers -- and what makes the va_end rule below
* checkable in one place instead of three. * 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 * 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 * 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 * line of program output without anything visibly misbehaving, which is
* exactly what made it worth fixing before something did. TODO.md 2.1.4. * exactly what made it worth fixing before something did.
* - *count is written on every path. It used to be left holding vprintf's -1 * - *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 * 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 * 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 * 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 * error-handling wrapper around an unbounded write is precisely the sharp edge
* this library exists to remove. aksl_snprintf replaces it, and treats * this library exists to remove. aksl_snprintf replaces it, and treats
* truncation as the failure it is rather than as a short success. TODO.md 2.2.4. * truncation as the failure it is rather than as a short success.
*/ */
akerr_ErrorContext AKERR_NOIGNORE *aksl_vprintf(int *count, const char *restrict format, va_list args) 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 * 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 * 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 * it that nothing can execute -- dead code that reads as though there were a
* failure mode to catch, which is exactly what TODO.md 2.2.11 recorded * failure mode to catch, which is exactly what was recorded
* against the old aksl_memset and aksl_memcpy and what removing those was * 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. * 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("99999999999999999999") handed back success and a wrapped value, because
* atoi(3) has no error channel at all. A library whose entire value proposition * 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. * is turning silent libc failures into error contexts cannot ship that.
* TODO.md 2.1.5. * See UPGRADING.md.
* *
* The strto* wrappers below are the real implementation and the ato* wrappers * The strto* wrappers below are the real implementation and the ato* wrappers
* are three-line calls into them, which is what TODO.md 3.1 asked for on its own * are three-line calls into them, which is what the wrapper contract asked for on its own
* account -- akbasic had to hand-write ~60 lines of exactly this (its * account -- akbasic had to hand-write ~60 lines of exactly this (its
* src/convert.c) because the library would not do it. * 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. * realpath(3), with the destination buffer made expressible.
* *
* The old two-argument form had three separate problems, all of them reachable * The old two-argument form had three separate problems, all of them reachable
* from ordinary use (TODO.md 2.1.6): resolved_path was never NULL-checked, so * from ordinary use: resolved_path was never NULL-checked, so
* realpath(path, NULL) allocated a buffer the wrapper then discarded and leaked; * 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 * 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 * 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 * negative value: the hash differed from canonical djb2, and -- worse -- differed
* between platforms depending on the signedness of char. Benign for the 7-bit * 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 * ASCII identifiers akbasic hashes, and quietly wrong for the first caller to
* key a table on a filename or a UTF-8 string literal. TODO.md 2.2.6. * key a table on a filename or a UTF-8 string literal.
*/ */
akerr_ErrorContext AKERR_NOIGNORE *aksl_strhash_djb2(const char *str, size_t len, uint32_t *hashval) 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); SUCCEED_RETURN(e);
} }
/* The NUL-terminated convenience form TODO.md 3.6 asked for. */ /* The NUL-terminated convenience form the collections plan asked for. */
akerr_ErrorContext AKERR_NOIGNORE *aksl_strhash_djb2_str(const char *str, uint32_t *hashval) akerr_ErrorContext AKERR_NOIGNORE *aksl_strhash_djb2_str(const char *str, uint32_t *hashval)
{ {
PREPARE_ERROR(e); 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 * Two separate walks, deliberately. The Floyd pass answers "is this list
* finite"; it says nothing about where the tail is, because `slow` stops at * 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 * the midpoint. Conflating the two is what made this function truncate every
* list of two or more nodes -- see TODO.md 2.1.1. * list of two or more nodes.
*/ */
while ( fast != NULL && fast->next != NULL ) { while ( fast != NULL && fast->next != NULL ) {
slow = slow->next; 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 * `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 * 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 -- TODO.md 2.2.12. Taking * there was no way for the caller to learn the new one. Taking
* the head by reference makes the correct call the only call that compiles. * 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) 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 * 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 * before its first use, because append and iterate both read next/prev -- and a
* stack-allocated node that skipped it walked into garbage. TODO.md 2.2.14. * stack-allocated node that skipped it walked into garbage.
*/ */
akerr_ErrorContext AKERR_NOIGNORE *aksl_list_node_init(aksl_ListNode *node, void *data) 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 * 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 * 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 * allocate and release these -- which is what they were always documented to do
* and never actually did (TODO.md 2.2.8). * and never actually did.
*/ */
typedef struct TreeQueueEntry { typedef struct TreeQueueEntry {
struct TreeQueueEntry *next; 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 * 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 * 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 * it and returned success, the parent's PASS therefore saw nothing wrong, and
* the walk carried on into the sibling subtree -- TODO.md 2.1.3. * the walk carried on into the sibling subtree.
* *
* `path` is the chain of ancestors of `root` and `depth` its length. Checking * `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 * 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 * infinite recursion into AKERR_CIRCULAR_REFERENCE, and the depth cap catches the
* degenerate chain that is merely too deep to recurse over (TODO.md 2.2.7). * degenerate chain that is merely too deep to recurse over.
*/ */
static akerr_ErrorContext AKERR_NOIGNORE *tree_dfs_walk( static akerr_ErrorContext AKERR_NOIGNORE *tree_dfs_walk(
aksl_TreeNode *root, 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 -- * 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 * and AKSL_TREE_SEARCH_VISIT, which the header documented but nothing
* implemented -- fell straight through to success having visited nothing * implemented -- fell straight through to success having visited nothing
* (TODO.md 2.2.9). * See UPGRADING.md.
*/ */
switch ( searchmode ) { switch ( searchmode ) {
case AKSL_TREE_SEARCH_DFS_PREORDER: 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 * 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 * midpoint, and starting the visit there skipped the whole first half of the
* list including the head -- see TODO.md 2.1.2. * list including the head.
*/ */
while ( node != NULL ) { while ( node != NULL ) {
ATTEMPT { ATTEMPT {

View File

@@ -1,5 +1,5 @@
/* /*
* stdio.h wrappers beyond fopen/fread/fwrite/fclose -- TODO.md section 3.1. * stdio.h wrappers beyond fopen/fread/fwrite/fclose.
* *
* Positioning, flushing, character and line I/O, stream state, formatted input, * Positioning, flushing, character and line I/O, stream state, formatted input,
* and the file-level operations that go with them. * and the file-level operations that go with them.

View File

@@ -1,12 +1,12 @@
/* /*
* string.h wrappers -- TODO.md section 3.1. * string.h wrappers.
* *
* This is the section akbasic needed most and could not have: across its source * 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 * 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 * 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 * 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 * an explicit NUL -- which is exactly the "truncation reported as an error
* rather than silently accepted" that TODO.md 3.1 asks for. * rather than silently accepted" that the wrapper contract asks for.
* *
* Two conventions run through the whole file. * Two conventions run through the whole file.
* *

View File

@@ -1,5 +1,5 @@
/* /*
* NEGATIVE COMPILE TEST -- TODO.md sections 1.3 and 2.2.5. * NEGATIVE COMPILE TEST.
* *
* This file must NOT compile. It is built by the CTest entry * This file must NOT compile. It is built by the CTest entry
* `negative_format_mismatch` with -Werror, and that test is marked WILL_FAIL. * `negative_format_mismatch` with -Werror, and that test is marked WILL_FAIL.

View File

@@ -1,5 +1,5 @@
/* /*
* NEGATIVE COMPILE TEST -- TODO.md section 1.9. * NEGATIVE COMPILE TEST.
* *
* This file must NOT compile. It is built by the CTest entry `negative_noignore` * 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 * with -Werror, and that test is marked WILL_FAIL, so a successful build is a

View File

@@ -1,5 +1,5 @@
/* /*
* List and tree additions -- src/collections.c, TODO.md section 3.6. * List and tree additions -- src/collections.c.
* *
* The bare-node list functions, the tracked aksl_List container, and the binary * 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. * 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 * 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 * 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 * and exhaust the pool -- the failure mode the whole pool-accounting section of
* TODO.md 1.9 exists to catch. * the cross-cutting wrapper contract exists to catch.
*/ */
static akerr_ErrorContext AKERR_NOIGNORE *free_that_always_fails(void *ptr) static akerr_ErrorContext AKERR_NOIGNORE *free_that_always_fails(void *ptr)
{ {
@@ -800,7 +800,7 @@ static int test_tree_insert_orders_the_leaves(void)
} }
/* /*
* TODO.md 2.2.15: aksl_TreeNode.parent was declared and never touched by * aksl_TreeNode.parent was declared and never touched by
* anything in the library. These are the functions that set it, and * anything in the library. These are the functions that set it, and
* aksl_tree_remove is the one that needs it. * aksl_tree_remove is the one that needs it.
*/ */

View File

@@ -1,7 +1,7 @@
/* /*
* String -> number wrappers: aksl_atoi / atol / atoll / atof. * String -> number wrappers: aksl_atoi / atol / atoll / atof.
* *
* TODO.md section 1.4, complete. The cases that used to live in * Numeric conversion, complete. The cases that used to live in
* tests/test_convert_strict.c -- registered as a known failure because the ato* * 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 * 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 * 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 * "0x10" through the ato* forms is base 10, so parsing stops at the 'x' and the
* rest is trailing junk. TODO.md 1.4 left this open; the answer is that the * rest is trailing junk. The wrapper plan left this open; the answer is that the
* prefix-honouring parse is aksl_strtol(nptr, NULL, 0, &dest), and tests/ * prefix-honouring parse is aksl_strtol(nptr, NULL, 0, &dest), and tests/
* test_strto.c holds that half. * test_strto.c holds that half.
*/ */
@@ -149,7 +149,7 @@ static int test_atoi_narrows_to_int_range(void)
return 0; return 0;
} }
/* TODO.md 1.4: the type boundaries round-trip exactly rather than nearly. */ /* The type boundaries round-trip exactly rather than nearly. */
static int test_type_boundaries_round_trip(void) static int test_type_boundaries_round_trip(void)
{ {
char buf[64]; char buf[64];
@@ -229,7 +229,7 @@ static int test_atof_converts_and_rejects_null(void)
} }
/* /*
* TODO.md 1.4 left "inf" and "nan" open. They are accepted, because strtod(3) * The wrapper plan left "inf" and "nan" open. They are accepted, because strtod(3)
* accepts them and because they are exact round-trips rather than approximations * 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 * of something else -- a caller who did not want them has a domain check to make
* that this library cannot make for it. * that this library cannot make for it.

View File

@@ -2,7 +2,7 @@
* Formatted-output wrappers: aksl_printf / aksl_fprintf / aksl_snprintf and * Formatted-output wrappers: aksl_printf / aksl_fprintf / aksl_snprintf and
* their va_list forms. * their va_list forms.
* *
* TODO.md section 1.3, now complete. Each happy path asserts both halves of the * Formatted output, complete. Each happy path asserts both halves of the
* contract -- the byte count handed back through *count and the text that * contract -- the byte count handed back through *count and the text that
* actually landed somewhere -- and every pointer argument is checked for its * actually landed somewhere -- and every pointer argument is checked for its
* NULL guard. * NULL guard.
@@ -11,7 +11,7 @@
* points at the formatted-output wrapper under test and not at the stream * points at the formatted-output wrapper under test and not at the stream
* wrappers, which tests/test_stream.c covers. * wrappers, which tests/test_stream.c covers.
* *
* aksl_sprintf is gone (TODO.md 2.2.4) and aksl_snprintf takes its place, so the * aksl_sprintf is gone and aksl_snprintf takes its place, so the
* destination-overflow case that could not previously be written is here: it is * destination-overflow case that could not previously be written is here: it is
* AKERR_OUTOFBOUNDS, not the short success snprintf(3) would have reported. * 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 * 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: * errno it saw (EBADF on glibc). *count is 0 afterwards, not vfprintf's -1:
* TODO.md 1.3 recorded the negative count as a contract gap, and this is the * The wrapper plan recorded the negative count as a contract gap, and this is the
* assertion that closes it. * assertion that closes it.
*/ */
static int test_fprintf_to_read_only_stream_reports_errno(void) 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 TODO.md 3.1 * The va_list forms are what the variadic ones are built on, and the wrapper contract
* wanted them exposed so consumers can write their own variadic wrappers. This * wanted them exposed so consumers can write their own variadic wrappers. This
* is a consumer doing exactly that. * 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 (TODO.md 2.1.4). Nothing here can * Regression cover for the missing va_end. Nothing here can
* assert on register-save state directly; the point is to run the variadic * assert on register-save state directly; the point is to run the variadic
* wrappers enough times, with enough arguments, that the sanitizer build has * wrappers enough times, with enough arguments, that the sanitizer build has
* something to trip over. * something to trip over.

View File

@@ -1,5 +1,5 @@
/* /*
* The fixed-capacity hash map and FNV-1a -- src/collections.c, TODO.md 3.6. * The fixed-capacity hash map and FNV-1a -- src/collections.c.
* *
* "The single most obviously-missing data structure in the library", by the * "The single most obviously-missing data structure in the library", by the
* TODO's own account: akbasic needed one three times over -- variables, * TODO's own account: akbasic needed one three times over -- variables,

View File

@@ -1,5 +1,5 @@
/* /*
* Linked list -- TODO.md section 1.7, complete. * Linked list.
* *
* The two confirmed list defects are fixed, so the tests that used to live in * 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 * 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
/* ---------------------------------------------------------------------- */ /* ---------------------------------------------------------------------- */
/* /*
* TODO.md 2.2.14: every caller used to have to remember to memset a node before * 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. * its first use, and a stack node that skipped it walked straight into garbage.
*/ */
static int test_node_init_zeroes_the_links(void) 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 * 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 * 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 * the midpoint rather than the last node. Appending n1..n4 to n0 produced the
* chain "n0 -> n4" and silently dropped n1, n2 and n3. TODO.md 2.1.1. * chain "n0 -> n4" and silently dropped n1, n2 and n3.
*/ */
static int test_append_builds_the_whole_chain(void) static int test_append_builds_the_whole_chain(void)
{ {
@@ -218,7 +218,7 @@ static int test_append_detects_cycle_below_the_head(void)
} }
/* /*
* TODO.md 1.7 asked for the aliasing contract to be defined. It is refusal: * The wrapper plan asked for the aliasing contract to be defined. It is refusal:
* relinking a node that is already in the list would orphan everything between * 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 -- * its old position and the tail, so the tail walk -- which happens anyway --
* doubles as the check. * 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 * 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 * 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 * to start from there -- so the whole first half of the list, head included, was
* never passed to the callback at all. TODO.md 2.1.2. * never passed to the callback at all.
*/ */
static int test_iterate_visits_every_node_from_the_head(void) static int test_iterate_visits_every_node_from_the_head(void)
{ {
@@ -470,7 +470,7 @@ static int test_pop_middle_node(void)
} }
/* /*
* TODO.md 2.2.12: popping the head used to leave the caller's own head pointer * 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 * 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. * 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)
} }
/* /*
* TODO.md 1.7's pool-accounting case. AKSL_RUN already asserts that each test * The wrapper plan'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 * 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 * 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 * error and forgets to release it shows up as an outright exhaustion rather than

View File

@@ -1,8 +1,8 @@
/* /*
* Memory wrappers -- TODO.md section 1.1, now complete, plus the additions from * Memory wrappers, complete, plus the additions from
* section 3.1. * section 3.1.
* *
* The three cases 1.1 left open are all pinned here: malloc(0) is AKERR_VALUE * The three cases the wrapper plan left open are all pinned here: malloc(0) is AKERR_VALUE
* rather than whatever errno happened to hold when the platform's malloc(0) * rather than whatever errno happened to hold when the platform's malloc(0)
* returned NULL; an allocation the system cannot satisfy reports ENOMEM and * returned NULL; an allocation the system cannot satisfy reports ENOMEM and
* leaves *dst NULL rather than garbage; and overlapping memcpy is refused with * leaves *dst NULL rather than garbage; and overlapping memcpy is refused with
@@ -32,7 +32,7 @@ static int test_malloc_rejects_null_destination(void)
} }
/* /*
* TODO.md 1.1 asked for this contract to be pinned down. malloc(0) is allowed to * The wrapper plan 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 * 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 * 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 * 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)
} }
/* /*
* TODO.md 1.1's open question, decided: overlap is AKERR_VALUE. memcpy(3) calls * The wrapper plan's open question, decided: overlap is AKERR_VALUE. memcpy(3) calls
* it undefined behaviour, which in practice means "works until the day a * 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 * compiler version or a length changes and it does not". Callers who mean to
* overlap want aksl_memmove, and the message says so. * overlap want aksl_memmove, and the message says so.

View File

@@ -1,12 +1,12 @@
/* /*
* aksl_realpath and aksl_realpath_alloc -- TODO.md section 1.5, now complete. * aksl_realpath and aksl_realpath_alloc.
* *
* The happy paths compare against realpath(3) itself rather than against a * 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 * 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. * 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 * The failure cases now pass an *uninitialised* resolved_path on purpose. That
* used to be the crash case (TODO.md 2.1.6): the wrapper's own error path * used to be the crash case: the wrapper's own error path
* formatted the buffer with %s while realpath(3) leaves its contents * formatted the buffer with %s while realpath(3) leaves its contents
* unspecified on failure, so the library read uninitialised memory while * unspecified on failure, so the library read uninitialised memory while
* reporting an error. The message names only the input path now, and this test * 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)), AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_realpath(NULL, resolved, sizeof(resolved)),
AKERR_NULLPOINTER, "path="); AKERR_NULLPOINTER, "path=");
/* /*
* TODO.md 2.1.6: this used to be unchecked, and realpath(path, NULL) * this used to be unchecked, and realpath(path, NULL)
* allocated a buffer that the wrapper then discarded and leaked. * allocated a buffer that the wrapper then discarded and leaked.
*/ */
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_realpath("/tmp", NULL, PATH_MAX), AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_realpath("/tmp", NULL, PATH_MAX),

View File

@@ -1,5 +1,5 @@
/* /*
* Cross-cutting properties every wrapper has to have -- TODO.md section 1.9. * Cross-cutting properties every wrapper has to have.
* *
* Two of them, and neither is about what any individual function computes: * Two of them, and neither is about what any individual function computes:
* *

134
tests/test_stat.c Normal file
View File

@@ -0,0 +1,134 @@
/* File metadata wrapper tests. */
#include "aksl_capture.h"
#include <errno.h>
#include <fcntl.h>
#include <string.h>
/*
* A bit libc has never assigned in the AT_ space. fstatat(2) must reject it
* with EINVAL rather than ignoring it, which is what proves flags reach libc
* unmasked. Re-check this constant if AT_ ever grows into 0x40000000.
*/
#define AKSL_TEST_AT_INVALID 0x40000000
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_STATUS(aksl_fstatvfs(fd, &vfs_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 directory[AKSL_TMP_MAX];
char linkpath[AKSL_TMP_MAX * 2];
char *basename = NULL;
int dirfd = -1;
struct stat dest;
struct stat ldest;
struct stat path_dest;
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);
AKSL_CHECK_STATUS(aksl_lstat("/nonexistent/aksl/stat", &ldest), 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_OK(aksl_stat(path, &path_dest));
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));
/* A real dirfd must resolve a relative name against that directory. */
AKSL_CHECK(snprintf(directory, sizeof(directory), "%s", path) < (int)sizeof(directory));
basename = strrchr(directory, '/');
AKSL_CHECK(basename != NULL);
*basename = '\0';
dirfd = open(directory, O_RDONLY | O_DIRECTORY);
AKSL_CHECK(dirfd >= 0);
AKSL_CHECK_OK(aksl_fstatat(dirfd, strrchr(path, '/') + 1, &dest, 0));
AKSL_CHECK(dest.st_ino == path_dest.st_ino);
AKSL_CHECK(close(dirfd) == 0);
/* A valid non-zero flag must reach libc, making this call an lstat. */
AKSL_CHECK_OK(aksl_fstatat(AT_FDCWD, linkpath, &dest, AT_SYMLINK_NOFOLLOW));
AKSL_CHECK(S_ISLNK(dest.st_mode));
/* Invalid flags must replace stale errno with the EINVAL libc reports. */
errno = E2BIG;
AKSL_CHECK_STATUS(aksl_fstatat(AT_FDCWD, path, &dest, AKSL_TEST_AT_INVALID), EINVAL);
/* statvfs reports ENOENT before it can describe a missing filesystem path. */
AKSL_CHECK_STATUS(aksl_statvfs("/nonexistent/aksl/stat", &vdest), ENOENT);
/* 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);
}

View File

@@ -1,5 +1,5 @@
/* /*
* The growable string buffer -- src/collections.c, TODO.md 3.6. * The growable string buffer -- src/collections.c.
* *
* The bounded formatting wrappers are the right answer when the destination is * 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. * a fixed buffer and no answer at all when the length is not known in advance.

View File

@@ -1,7 +1,7 @@
/* /*
* Stream wrappers: aksl_fopen / aksl_fread / aksl_fwrite / aksl_fclose. * Stream wrappers: aksl_fopen / aksl_fread / aksl_fwrite / aksl_fclose.
* *
* TODO.md section 1.2, now complete. The happy paths, the round trip, every * Stream wrappers, complete. The happy paths, the round trip, every
* NULL guard, both stream-error statuses, the transferred-member count that * 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 * 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 * 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), AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_fopen(path, "r", NULL),
AKERR_NULLPOINTER, "fp="); AKERR_NULLPOINTER, "fp=");
/* TODO.md 2.2.2: both of these used to go straight through to fopen(3). */ /* both of these used to go straight through to fopen(3). */
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_fopen(NULL, "r", &fp), AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_fopen(NULL, "r", &fp),
AKERR_NULLPOINTER, "pathname="); AKERR_NULLPOINTER, "pathname=");
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_fopen(path, NULL, &fp), 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_OK(aksl_fopen(path, "r", &fp));
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_fread(buf, 1, sizeof(buf), fp, &moved), AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_fread(buf, 1, sizeof(buf), fp, &moved),
AKERR_EOF, "EOF"); AKERR_EOF, "EOF");
/* TODO.md 2.2.3: this is what the caller could not previously find out. */ /* this is what the caller could not previously find out. */
AKSL_CHECK(moved == 4); AKSL_CHECK(moved == 4);
AKSL_CHECK_OK(aksl_fclose(fp)); 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 * 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 * AKERR_IO without ever consulting errno. Routing it through AKSL_ERRNO_OR
* (TODO.md 2.2.1) keeps AKERR_IO as the fallback for the case where the stream * 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. * 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) 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), AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_fread(buf, 1, sizeof(buf), NULL, &moved),
AKERR_NULLPOINTER, "fp="); AKERR_NULLPOINTER, "fp=");
/* TODO.md 2.2.3: ptr was never checked in either direction. */ /* ptr was never checked in either direction. */
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_fread(NULL, 1, sizeof(buf), fp, &moved), AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_fread(NULL, 1, sizeof(buf), fp, &moved),
AKERR_NULLPOINTER, "ptr="); AKERR_NULLPOINTER, "ptr=");
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_fread(buf, 1, sizeof(buf), fp, NULL), AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_fread(buf, 1, sizeof(buf), fp, NULL),

View File

@@ -1,5 +1,5 @@
/* /*
* Stream wrappers beyond open/read/write/close -- src/stream.c, TODO.md 3.1. * Stream wrappers beyond open/read/write/close -- src/stream.c.
* *
* tests/test_stream.c covers fopen/fread/fwrite/fclose. This one covers * tests/test_stream.c covers fopen/fread/fwrite/fclose. This one covers
* positioning, flushing, character and line I/O, stream state, and formatted * positioning, flushing, character and line I/O, stream state, and formatted

View File

@@ -1,5 +1,5 @@
/* /*
* aksl_strhash_djb2 -- TODO.md section 1.6. * aksl_strhash_djb2.
* *
* The expected values are the canonical djb2 ones: h = 5381, then * 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 * 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 * 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 * the test therefore says nothing either way about byte signedness. The high-bit
* vector is the one that pins TODO.md 2.2.6 down. * vector is the one that pins the sign-extension defect down.
*/ */
#include "aksl_capture.h" #include "aksl_capture.h"
@@ -47,7 +47,7 @@ static int test_known_answer_vectors(void)
} }
/* /*
* TODO.md 2.2.6, pinned. The cursor is an unsigned char * now, so bytes at or * 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 * 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 * ARM Linux made them negative, giving 5859874 here instead of 5868578 -- a hash
* that disagreed with canonical djb2 and, worse, disagreed with itself across * 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; return 0;
} }
/* The NUL-terminated convenience form (TODO.md 3.6) agrees with the length one. */ /* The NUL-terminated convenience form agrees with the length one. */
static int test_str_form_matches_the_length_form(void) static int test_str_form_matches_the_length_form(void)
{ {
const char *s = "libakstdlib"; const char *s = "libakstdlib";

View File

@@ -1,5 +1,5 @@
/* /*
* String wrappers -- src/string.c, TODO.md section 3.1. * String wrappers -- src/string.c.
* *
* The two contracts worth testing hardest are the ones that differ from libc: * 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 * every copying function takes the destination size and treats truncation as an

View File

@@ -1,5 +1,5 @@
/* /*
* The strto* family -- TODO.md section 3.1. * The strto* family.
* *
* These are the real implementation behind the ato* wrappers and the thing * 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 * akbasic had to hand-write for itself (its src/convert.c, ~60 lines) because

View File

@@ -1,5 +1,5 @@
/* /*
* Tree traversal -- TODO.md section 1.8, complete. * Tree traversal.
* *
* The old version of this file counted steps, which cannot tell the three * 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 * 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 * BFS was AKERR_NOT_IMPLEMENTED, and the lalloc/lfree parameters that existed to
* serve it were defaulted and then never called -- TODO.md 2.2.8 and 2.2.10. * serve it were defaulted and then never called. See UPGRADING.md.
* Both modes work now, and the allocator test below proves the queue is real. * Both modes work now, and the allocator test below proves the queue is real.
*/ */
static int test_bfs_visits_level_by_level(void) static int test_bfs_visits_level_by_level(void)
@@ -249,7 +249,7 @@ static akerr_ErrorContext AKERR_NOIGNORE *counting_free(void *ptr)
} }
/* /*
* TODO.md 1.8: "Custom lalloc/lfree are actually invoked -- currently they are * The wrapper plan asked that "custom lalloc/lfree are actually invoked -- currently they are
* stored and never called". They are called now, once per node enqueued, and * 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, * every allocation is released. The depth-first modes allocate nothing at all,
* which is the other half of the contract. * which is the other half of the contract.
@@ -382,7 +382,7 @@ static int test_degenerate_chains(void)
} }
/* /*
* TODO.md 1.8 / 2.2.7: a chain deeper than the recursion can take. It used to * A chain deeper than the recursion can take. It used to
* overflow the stack; it is AKERR_OUTOFBOUNDS now, and the message names the * 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 * 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 * 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 * 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 * 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 * into the sibling subtree. All seven nodes were visited no matter where the
* break was raised. TODO.md 2.1.3. * break was raised.
* *
* One case per order, each breaking on a node that is *not* last in that order -- * 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 * 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)
} }
/* /*
* TODO.md 2.2.9: the switch had no default, so an unrecognised mode -- and * the switch had no default, so an unrecognised mode -- and
* AKSL_TREE_SEARCH_VISIT, which the header documented but nothing implemented -- * AKSL_TREE_SEARCH_VISIT, which the header documented but nothing implemented --
* fell straight through to SUCCEED_RETURN having visited nothing at all. A * fell straight through to SUCCEED_RETURN having visited nothing at all. A
* traversal that silently did not happen, reported as success. * traversal that silently did not happen, reported as success.

View File

@@ -1,5 +1,5 @@
/* /*
* Version reporting -- TODO.md section 2.2.16. * Version reporting.
* *
* There are two versions in play and the whole point of this API is that they * There are two versions in play and the whole point of this API is that they
* are allowed to differ: * are allowed to differ: