Compare commits
36 Commits
todo-to-is
...
26
| Author | SHA1 | Date | |
|---|---|---|---|
| cff2a64575 | |||
|
2b79aca103
|
|||
|
15e9104d9e
|
|||
|
01034fc668
|
|||
|
8c94231167
|
|||
|
be725f8cf2
|
|||
|
58f426abce
|
|||
|
55d986c631
|
|||
| d6a1cd8ca8 | |||
|
c95f8e9770
|
|||
|
22208f0aa0
|
|||
|
8a76b88648
|
|||
|
c0c18f5a6a
|
|||
|
c0a1c18c87
|
|||
|
602759e491
|
|||
|
90134ca0f6
|
|||
|
8c1746aba6
|
|||
|
b22de3e34c
|
|||
|
a2ec96e88f
|
|||
|
c61e59b9a5
|
|||
|
a5b19f1d8d
|
|||
|
ac9f383a91
|
|||
|
024fe6cde2
|
|||
|
f60787a852
|
|||
|
681ca1d3c7
|
|||
|
be6d2f6d7c
|
|||
|
a776c5a568
|
|||
|
b63d1a0503
|
|||
|
c5386492b4
|
|||
|
bd401b2452
|
|||
|
ac570890f9
|
|||
|
54165a615b
|
|||
|
13efe5e91b
|
|||
|
7ca7d08d7e
|
|||
|
824272a4d7
|
|||
|
fe0d1145a1
|
@@ -21,7 +21,7 @@ jobs:
|
||||
# different libakerrors depending on where you built: the install went to
|
||||
# /usr/local, while the build below is top-level and so compiles
|
||||
# deps/libakerror at the pinned commit via add_subdirectory -- the
|
||||
# installed one was never actually linked against. TODO.md 2.3.
|
||||
# installed one was never actually linked against.
|
||||
#
|
||||
# The submodule wins. It is the version this repository pins, tests and
|
||||
# ships against, and a CI that tests a different one is testing something
|
||||
@@ -155,7 +155,7 @@ jobs:
|
||||
# new surface being argument validation whose mutants are frequently
|
||||
# equivalent. `errno = 0` deleted from a wrapper whose libc call always
|
||||
# sets errno cannot be distinguished by any test that could be written.
|
||||
# The survivors worth acting on are named in TODO.md; raise the gate as
|
||||
# The survivors worth acting on are named in issue #7; raise the gate as
|
||||
# they become assertions.
|
||||
- name: mutation testing
|
||||
run: |
|
||||
|
||||
@@ -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
|
||||
of them is wrong even if it compiles and passes:
|
||||
|
||||
- **Preserve the libc contract unless there is a compelling, documented reason
|
||||
not to.** libc behaviour is the standard to meet. This library changes only
|
||||
the error transport (to `akerror`) and, where libc returns a value, the result
|
||||
shape (through a caller-provided destination pointer).
|
||||
- **A NULL out-param is a caller error**, not "don't care".
|
||||
- **Finding nothing is success** -- searching functions write NULL or zero and
|
||||
return NULL.
|
||||
@@ -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
|
||||
for the ..." on their own expansion, not on anything at the call site.
|
||||
|
||||
**Do not wrap a libc function that cannot fail and provides no failure or
|
||||
operation-status result.** There is no `akerror` context to carry. A value such
|
||||
as `umask()`'s previous mask is not an operation-status result, so `umask()` is
|
||||
not a wrapper candidate.
|
||||
|
||||
## Testing Guidelines
|
||||
|
||||
Add a new test by creating `tests/test_mything.c` and adding `mything` to the
|
||||
|
||||
@@ -5,7 +5,7 @@ cmake_minimum_required(VERSION 3.10)
|
||||
# akstdlibConfigVersion.cmake. Nothing else should spell a version number.
|
||||
#
|
||||
# 0.2.0, and the minor bump is an ABI break on purpose. Fixing the confirmed
|
||||
# defects 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:
|
||||
#
|
||||
# aksl_realpath takes the destination's length; aksl_realpath_alloc is new
|
||||
@@ -54,7 +54,7 @@ if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
|
||||
endif()
|
||||
|
||||
# Warnings. The only warning in the tree when these went on was the unused
|
||||
# `queue` parameter of aksl_tree_iterate, which 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
|
||||
# already-known defect, and the cost of leaving them off was every future one.
|
||||
#
|
||||
@@ -63,7 +63,7 @@ endif()
|
||||
# requires at least one argument for the ...", which is a complaint about the
|
||||
# macro's shape rather than about anything at this call site. Every FAIL_* in
|
||||
# src/stdlib.c passes at least one argument regardless -- see the note in
|
||||
# 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)$")
|
||||
add_compile_options(-Wall -Wextra)
|
||||
# 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
|
||||
# dropping it: its coverage script drives its own instrumented build tree, so
|
||||
# `cmake --build build-coverage --target akerror_coverage` still does the right
|
||||
# thing. Remove this once the dependency namespaces it upstream -- see TODO.md.
|
||||
# thing. Remove this once the dependency namespaces it upstream -- libakerror issue #15.
|
||||
function(add_custom_target _name)
|
||||
if(AKSL_SUPPRESS_ADD_TEST AND _name STREQUAL "coverage")
|
||||
_add_custom_target(akerror_coverage ${ARGN})
|
||||
@@ -207,6 +207,7 @@ add_library(akstdlib SHARED
|
||||
src/stdlib.c
|
||||
src/string.c
|
||||
src/stream.c
|
||||
src/stat.c
|
||||
src/collections.c
|
||||
)
|
||||
|
||||
@@ -317,6 +318,7 @@ set(AKSL_TESTS
|
||||
strbuf
|
||||
stream
|
||||
streamio
|
||||
stat
|
||||
strhash
|
||||
string
|
||||
strto
|
||||
@@ -329,7 +331,7 @@ set(AKSL_WILL_FAIL_TESTS
|
||||
|
||||
# Empty, and that is the news. It held four entries -- convert_strict,
|
||||
# list_append_chain, list_iterate_head and tree_iterate_break -- one for each
|
||||
# confirmed defect 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/
|
||||
# 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.
|
||||
@@ -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})
|
||||
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
|
||||
# else: AKERR_NOIGNORE makes discarding a returned error context an error, and
|
||||
|
||||
43
README.md
43
README.md
@@ -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.
|
||||
|
||||
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.
|
||||
|
||||
## What it wraps
|
||||
@@ -180,8 +181,9 @@ would notice.
|
||||
|
||||
## Testing
|
||||
|
||||
There are four harnesses. The first three take seconds; the fourth takes about
|
||||
half an hour.
|
||||
There are five harnesses. The first three take seconds and the fifth is instant;
|
||||
the fourth takes about half an hour. The fifth is the only one that measures
|
||||
something outside this repository.
|
||||
|
||||
### 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
|
||||
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
|
||||
|
||||
`.githooks/pre-push` runs the fast harnesses — the default build and the
|
||||
|
||||
126
TODO.md
126
TODO.md
@@ -22,6 +22,7 @@ it has been through grooming.
|
||||
| Function coverage | 100% (154/154) |
|
||||
| Doxygen | 100% of 154, gated — `cmake --build build --target docs` fails on an undocumented function, parameter or return |
|
||||
| Mutation score | 72.3% (188/260 sampled from 1701), gated at 65 |
|
||||
| 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
|
||||
`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
|
||||
|
||||
`akbasic` (`source.starfort.tech/andrew/akbasic`) is a ~6,300-line C interpreter
|
||||
built on this library and `libakerror`. It was the first consumer to exercise the
|
||||
whole surface rather than a corner of it, **and what it could not use is what
|
||||
prioritised everything that has been built since.**
|
||||
`akbasic` (`source.starfort.tech/andrew/akbasic`) is a C interpreter built on this
|
||||
library and `libakerror` — ~6,300 lines of `src/` when it was first measured,
|
||||
20,169 now. It was the first consumer to exercise the whole surface rather than a
|
||||
corner of it, **and what it could not use is what prioritised everything that has
|
||||
been built since.**
|
||||
|
||||
**The number that started it.** Across `src/`, akbasic made **10 calls into this
|
||||
library and 116 to raw libc** — a library whose value proposition is "turn silent
|
||||
libc failures into error contexts", bypassed 92% of the time by the consumer most
|
||||
committed to it.
|
||||
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 committed to it.
|
||||
|
||||
| Raw libc it had to use | Count | Now available as |
|
||||
|---|---|---|
|
||||
@@ -121,8 +123,17 @@ committed to it.
|
||||
| `strncpy` | 15 | `aksl_strncpy` |
|
||||
| `strtoll` / `strtod` | 2 | `aksl_strtoll` / `aksl_strtod` |
|
||||
| `fgets` | 2 | `aksl_fgets` |
|
||||
| `strncmp` | 1 | `aksl_strncmp` |
|
||||
| `memmove` | 1 | `aksl_memmove` |
|
||||
| `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.**
|
||||
|
||||
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
|
||||
akbasic's stdio text sink ran on every line of program output — is fixed.
|
||||
|
||||
**Still true, and still shaping the wishlist.** akbasic uses no allocator, no lists
|
||||
and no trees, drawing everything from fixed pools by design. **A consumer that does
|
||||
allocate would weight the `open`/`read`/`write` work far higher than this one
|
||||
does**, so one consumer's count is evidence, not a plan. Re-counting against this
|
||||
release is #26.
|
||||
### The recount, against this release
|
||||
|
||||
akbasic's `src/` was ported onto 0.2.0 and counted again (#26). The port builds
|
||||
clean at `-Wall -Wextra`, passes **112/112** of akbasic's ctest suite, and is
|
||||
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.
|
||||
|
||||
@@ -26,7 +26,8 @@
|
||||
* takes a slot from it on any failure path. See README.md.
|
||||
*
|
||||
* @see README.md for the deviations from libc semantics, UPGRADING.md if you are
|
||||
* coming from 0.1.0, 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_
|
||||
@@ -69,12 +70,15 @@
|
||||
*
|
||||
* It used to pull in stdlib.h and string.h as well, which nothing here needs and
|
||||
* which every consumer then got whether it wanted them or not. stddef.h in place
|
||||
* of stdlib.h is the same size_t at a fraction of the namespace. TODO.md 2.2.16.
|
||||
* of stdlib.h is the same size_t at a fraction of the namespace.
|
||||
*/
|
||||
#include <stdarg.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.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. */
|
||||
#include <sys/types.h>
|
||||
|
||||
@@ -85,7 +89,7 @@ extern "C" {
|
||||
/*
|
||||
* Restore the compile-time format/argument checking that callers otherwise lose
|
||||
* by going through a variadic wrapper: without it, printf("%d", "str") is caught
|
||||
* and aksl_printf(&n, "%d", "str") is not. 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
|
||||
* 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
|
||||
*
|
||||
|
||||
245
scripts/consumer_calls.py
Executable file
245
scripts/consumer_calls.py
Executable 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())
|
||||
@@ -18,7 +18,7 @@
|
||||
* the context still holds a pool slot: an error that is invisible and leaks at
|
||||
* the same time. Every errno-sourced status in this library goes through here,
|
||||
* and every wrapped call clears errno first so the value read back is its own.
|
||||
* TODO.md 2.2.1.
|
||||
* See UPGRADING.md.
|
||||
*/
|
||||
#define AKSL_ERRNO_OR(__fallback) (errno != 0 ? errno : (__fallback))
|
||||
|
||||
|
||||
@@ -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
|
||||
* visibly missing, plus the two structures the first real consumer had to write
|
||||
@@ -328,7 +328,7 @@ akerr_ErrorContext AKERR_NOIGNORE *aksl_list_iterate_reverse(aksl_ListNode *tail
|
||||
* aksl_list_append has to walk the whole list to find the tail, so building a
|
||||
* list of n nodes with it is O(n^2). That is fine for the handful of nodes the
|
||||
* bare-node API was written for and wrong for anything larger, which is what
|
||||
* 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
|
||||
* walk. It owns no memory -- the nodes are still the caller's -- so there is no
|
||||
@@ -440,7 +440,7 @@ akerr_ErrorContext AKERR_NOIGNORE *aksl_list_clear(aksl_List *list, aksl_FreeFun
|
||||
* through an out-param and may raise, like every other callback here.
|
||||
*
|
||||
* These are the functions that set and read aksl_TreeNode.parent, which was
|
||||
* declared and then never touched by anything in the library (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
|
||||
* node's parent about it, and finding the parent by walking from the root again
|
||||
* would turn a removal into a second search.
|
||||
@@ -691,7 +691,7 @@ akerr_ErrorContext AKERR_NOIGNORE *aksl_tree_free_all(aksl_TreeNode **root, aksl
|
||||
/* ====================================================================== */
|
||||
|
||||
/*
|
||||
* FNV-1a, the other half of 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
|
||||
* rather better -- worth having when the keys are short and share a prefix,
|
||||
* which is exactly what identifiers in a symbol table look like.
|
||||
@@ -730,7 +730,7 @@ akerr_ErrorContext AKERR_NOIGNORE *aksl_strhash_fnv1a_str(const char *str, uint3
|
||||
/*
|
||||
* Fixed-capacity, open-addressed, linear-probing, string-keyed.
|
||||
*
|
||||
* The shape is akbasic's src/symtab.c, which 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
|
||||
* rather than resizes when full, and the keys are copied into fixed-size slots
|
||||
* so the map owns them and a caller cannot outlive its own key strings.
|
||||
@@ -939,7 +939,7 @@ akerr_ErrorContext AKERR_NOIGNORE *aksl_hashmap_iterate(aksl_HashMap *map,
|
||||
*
|
||||
* The bounded formatting wrappers are the right answer when the destination is
|
||||
* a fixed buffer, and no answer at all when the output length is not known in
|
||||
* advance -- which is why 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
|
||||
* or a generated line means appending to something that grows.
|
||||
*
|
||||
|
||||
108
src/stat.c
Normal file
108
src/stat.c
Normal 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);
|
||||
}
|
||||
44
src/stdlib.c
44
src/stdlib.c
@@ -99,7 +99,7 @@ akerr_ErrorContext AKERR_NOIGNORE *aksl_calloc(size_t nmemb, size_t size, void *
|
||||
* block valid*, so the near-universal `p = realloc(p, n)` leaks the original
|
||||
* every time it fails. Here the old pointer goes in and out through the same
|
||||
* out-param, and is left untouched -- still valid, still the caller's to free --
|
||||
* whenever an error is raised. TODO.md 3.1.
|
||||
* whenever an error is raised.
|
||||
*/
|
||||
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.
|
||||
* 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
|
||||
* supposed to make unnecessary rather than merely possible.
|
||||
*/
|
||||
@@ -214,7 +214,7 @@ akerr_ErrorContext AKERR_NOIGNORE *aksl_freep(void **ptr)
|
||||
* memset(3) and memcpy(3) cannot fail. They return their destination pointer
|
||||
* unconditionally, so the old FAIL_ZERO_RETURN(e, memset(...), ...) and
|
||||
* (memcpy(...) == d) checks were dead code that read as though there were a
|
||||
* failure mode to catch -- 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.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *aksl_memset(void *s, int c, size_t n)
|
||||
@@ -289,7 +289,7 @@ akerr_ErrorContext AKERR_NOIGNORE *aksl_memchr(const void *s, int c, size_t n, v
|
||||
* pathname and mode are checked. fopen(NULL, ...) is undefined behaviour, and
|
||||
* this wrapper used to hand both straight through unexamined -- reachable from
|
||||
* user input in practice, which is why akbasic validates the filename itself
|
||||
* before calling DLOAD/DSAVE with a comment pointing at TODO.md 2.2.2.
|
||||
* before calling DLOAD/DSAVE with a comment pointing at.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *aksl_fopen(
|
||||
const char *pathname,
|
||||
@@ -312,7 +312,7 @@ akerr_ErrorContext AKERR_NOIGNORE *aksl_fopen(
|
||||
|
||||
/*
|
||||
* fread and fwrite both report how much they actually transferred, through a
|
||||
* required out-param. Three things were wrong with the old pair (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
|
||||
* no way to find out how much data had arrived before the stream ran out, which
|
||||
* makes the EOF status almost useless for the partial-read case it exists to
|
||||
@@ -405,7 +405,7 @@ akerr_ErrorContext AKERR_NOIGNORE *aksl_fclose(FILE *stream)
|
||||
* Formatted output.
|
||||
*
|
||||
* The va_list forms below do the work and the variadic forms are thin wrappers
|
||||
* over them, which is both what §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
|
||||
* checkable in one place instead of three.
|
||||
*
|
||||
@@ -415,7 +415,7 @@ akerr_ErrorContext AKERR_NOIGNORE *aksl_fclose(FILE *stream)
|
||||
* one. Omitting it is undefined behaviour per the standard and leaks
|
||||
* register-save state on some ABIs. akbasic's text sink ran this UB on every
|
||||
* line of program output without anything visibly misbehaving, which is
|
||||
* exactly what made it worth fixing before something did. 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
|
||||
* after a failure, so a caller who read the length rather than the status got
|
||||
* a negative byte count out of a function that had already failed. It is now
|
||||
@@ -426,7 +426,7 @@ akerr_ErrorContext AKERR_NOIGNORE *aksl_fclose(FILE *stream)
|
||||
* aksl_sprintf is gone. It wrapped vsprintf, which cannot be bounded, and an
|
||||
* error-handling wrapper around an unbounded write is precisely the sharp edge
|
||||
* this library exists to remove. aksl_snprintf replaces it, and treats
|
||||
* truncation as the failure it is rather than as a short success. 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)
|
||||
@@ -561,7 +561,7 @@ akerr_ErrorContext AKERR_NOIGNORE *aksl_vasprintf(int *count, char **dest, const
|
||||
* same arguments a few lines up, so the write cannot truncate. Guarding it
|
||||
* anyway would mean a failure branch nothing can reach and a free() beneath
|
||||
* it that nothing can execute -- dead code that reads as though there were a
|
||||
* failure mode to catch, which is exactly what 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
|
||||
* for. The invariant is stated here instead, where it can be read.
|
||||
*/
|
||||
@@ -590,10 +590,10 @@ akerr_ErrorContext AKERR_NOIGNORE *aksl_asprintf(int *count, char **dest, const
|
||||
* atoi("99999999999999999999") handed back success and a wrapped value, because
|
||||
* atoi(3) has no error channel at all. A library whose entire value proposition
|
||||
* is turning silent libc failures into error contexts cannot ship that.
|
||||
* TODO.md 2.1.5.
|
||||
* See UPGRADING.md.
|
||||
*
|
||||
* 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
|
||||
* src/convert.c) because the library would not do it.
|
||||
*
|
||||
@@ -846,7 +846,7 @@ akerr_ErrorContext AKERR_NOIGNORE *aksl_atof(const char *nptr, double *dest)
|
||||
* realpath(3), with the destination buffer made expressible.
|
||||
*
|
||||
* The old two-argument form had three separate problems, all of them reachable
|
||||
* from ordinary use (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;
|
||||
* there was no way for a caller to say how big the buffer was, so everyone had
|
||||
* to know to supply PATH_MAX bytes; and the failure path formatted resolved_path
|
||||
@@ -904,7 +904,7 @@ akerr_ErrorContext AKERR_NOIGNORE *aksl_realpath_alloc(const char *restrict path
|
||||
* negative value: the hash differed from canonical djb2, and -- worse -- differed
|
||||
* between platforms depending on the signedness of char. Benign for the 7-bit
|
||||
* ASCII identifiers akbasic hashes, and quietly wrong for the first caller to
|
||||
* key a table on a filename or a UTF-8 string literal. 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)
|
||||
{
|
||||
@@ -921,7 +921,7 @@ akerr_ErrorContext AKERR_NOIGNORE *aksl_strhash_djb2(const char *str, size_t len
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
/* The NUL-terminated convenience form 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)
|
||||
{
|
||||
PREPARE_ERROR(e);
|
||||
@@ -941,7 +941,7 @@ akerr_ErrorContext AKERR_NOIGNORE *aksl_list_append(aksl_ListNode *list, aksl_Li
|
||||
* Two separate walks, deliberately. The Floyd pass answers "is this list
|
||||
* finite"; it says nothing about where the tail is, because `slow` stops at
|
||||
* the midpoint. Conflating the two is what made this function truncate every
|
||||
* list of two or more nodes -- see TODO.md 2.1.1.
|
||||
* list of two or more nodes.
|
||||
*/
|
||||
while ( fast != NULL && fast->next != NULL ) {
|
||||
slow = slow->next;
|
||||
@@ -973,7 +973,7 @@ akerr_ErrorContext AKERR_NOIGNORE *aksl_list_append(aksl_ListNode *list, aksl_Li
|
||||
/*
|
||||
* `head` is required, not optional. Popping the head node used to leave the
|
||||
* caller's own head pointer aimed at a node that is no longer in the list, and
|
||||
* there was no way for the caller to learn the new one -- 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.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *aksl_list_pop(aksl_ListNode **head, aksl_ListNode *node)
|
||||
@@ -998,7 +998,7 @@ akerr_ErrorContext AKERR_NOIGNORE *aksl_list_pop(aksl_ListNode **head, aksl_List
|
||||
/*
|
||||
* Zeroing initialisers. Every caller previously had to remember to memset a node
|
||||
* before its first use, because append and iterate both read next/prev -- and a
|
||||
* stack-allocated node that skipped it walked into garbage. 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)
|
||||
{
|
||||
@@ -1027,7 +1027,7 @@ akerr_ErrorContext AKERR_NOIGNORE *aksl_tree_node_init(aksl_TreeNode *node, void
|
||||
* aksl_ListNode: the walk needs to carry each node's depth alongside it, which is
|
||||
* how a cyclic tree is stopped without a visited-set. The caller's lalloc/lfree
|
||||
* allocate and release these -- which is what they were always documented to do
|
||||
* and never actually did (TODO.md 2.2.8).
|
||||
* and never actually did.
|
||||
*/
|
||||
typedef struct TreeQueueEntry {
|
||||
struct TreeQueueEntry *next;
|
||||
@@ -1165,12 +1165,12 @@ static akerr_ErrorContext AKERR_NOIGNORE *tree_bfs_walk(
|
||||
* unwinds every frame up to the public entry point, which swallows it exactly
|
||||
* once. In the old single-function form the frame that raised the break handled
|
||||
* it and returned success, the parent's PASS therefore saw nothing wrong, and
|
||||
* the walk carried on into the sibling subtree -- 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
|
||||
* each node against its own ancestry turns a tree that loops back on itself from
|
||||
* infinite recursion into AKERR_CIRCULAR_REFERENCE, and the depth cap catches the
|
||||
* degenerate chain that is merely too deep to recurse over (TODO.md 2.2.7).
|
||||
* degenerate chain that is merely too deep to recurse over.
|
||||
*/
|
||||
static akerr_ErrorContext AKERR_NOIGNORE *tree_dfs_walk(
|
||||
aksl_TreeNode *root,
|
||||
@@ -1266,7 +1266,7 @@ akerr_ErrorContext AKERR_NOIGNORE *aksl_tree_iterate(
|
||||
* assume it. The switch used to have no default at all, so searchmode 99 --
|
||||
* and AKSL_TREE_SEARCH_VISIT, which the header documented but nothing
|
||||
* implemented -- fell straight through to success having visited nothing
|
||||
* (TODO.md 2.2.9).
|
||||
* See UPGRADING.md.
|
||||
*/
|
||||
switch ( searchmode ) {
|
||||
case AKSL_TREE_SEARCH_DFS_PREORDER:
|
||||
@@ -1328,7 +1328,7 @@ akerr_ErrorContext AKERR_NOIGNORE *aksl_list_iterate(aksl_ListNode *list, aksl_L
|
||||
/*
|
||||
* From `list`, not from `slow`. The Floyd pass above leaves `slow` at the
|
||||
* midpoint, and starting the visit there skipped the whole first half of the
|
||||
* list including the head -- see TODO.md 2.1.2.
|
||||
* list including the head.
|
||||
*/
|
||||
while ( node != NULL ) {
|
||||
ATTEMPT {
|
||||
|
||||
@@ -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,
|
||||
* and the file-level operations that go with them.
|
||||
|
||||
@@ -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
|
||||
* it calls strlen 37 times, strcmp 16, strncpy 15 and strstr once, every one of
|
||||
* them raw because there was nothing here to call instead. Ten of those sites
|
||||
* are the same idiom written out by hand -- a length check, then strncpy, then
|
||||
* an explicit NUL -- which is exactly the "truncation reported as an error
|
||||
* rather than silently accepted" that TODO.md 3.1 asks for.
|
||||
* rather than silently accepted" that the wrapper contract asks for.
|
||||
*
|
||||
* Two conventions run through the whole 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
|
||||
* `negative_format_mismatch` with -Werror, and that test is marked WILL_FAIL.
|
||||
|
||||
@@ -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`
|
||||
* with -Werror, and that test is marked WILL_FAIL, so a successful build is a
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* List and tree additions -- src/collections.c, 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
|
||||
* search tree. The hash map and string buffer have their own files.
|
||||
@@ -407,7 +407,7 @@ static akerr_ErrorContext AKERR_NOIGNORE *free_that_fails_once(void *ptr)
|
||||
* single walk. Only the first is kept and handed back; the rest have to be
|
||||
* released, or a walk over n nodes with a broken free would consume n pool slots
|
||||
* and exhaust the pool -- the failure mode the whole pool-accounting section of
|
||||
* 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)
|
||||
{
|
||||
@@ -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
|
||||
* aksl_tree_remove is the one that needs it.
|
||||
*/
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/*
|
||||
* 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*
|
||||
* family had no error channel at all (2.1.5) -- are folded back in here now that
|
||||
* they pass: non-numeric input, empty input, trailing junk and overflow are all
|
||||
@@ -95,7 +95,7 @@ static int test_trailing_junk_is_a_value_error(void)
|
||||
|
||||
/*
|
||||
* "0x10" through the ato* forms is base 10, so parsing stops at the 'x' and the
|
||||
* rest is trailing junk. 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/
|
||||
* test_strto.c holds that half.
|
||||
*/
|
||||
@@ -149,7 +149,7 @@ static int test_atoi_narrows_to_int_range(void)
|
||||
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)
|
||||
{
|
||||
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
|
||||
* of something else -- a caller who did not want them has a domain check to make
|
||||
* that this library cannot make for it.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* Formatted-output wrappers: aksl_printf / aksl_fprintf / aksl_snprintf and
|
||||
* their va_list forms.
|
||||
*
|
||||
* 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
|
||||
* actually landed somewhere -- and every pointer argument is checked for its
|
||||
* NULL guard.
|
||||
@@ -11,7 +11,7 @@
|
||||
* points at the formatted-output wrapper under test and not at the stream
|
||||
* wrappers, which tests/test_stream.c covers.
|
||||
*
|
||||
* aksl_sprintf is gone (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
|
||||
* AKERR_OUTOFBOUNDS, not the short success snprintf(3) would have reported.
|
||||
*/
|
||||
@@ -139,7 +139,7 @@ static int test_fprintf_writes_to_stream(void)
|
||||
/*
|
||||
* vfprintf on a stream opened "r" fails outright, so the wrapper reports the
|
||||
* errno it saw (EBADF on glibc). *count is 0 afterwards, not vfprintf's -1:
|
||||
* 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.
|
||||
*/
|
||||
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
|
||||
* 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
|
||||
* wrappers enough times, with enough arguments, that the sanitizer build has
|
||||
* something to trip over.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* The fixed-capacity hash map and FNV-1a -- src/collections.c, 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
|
||||
* TODO's own account: akbasic needed one three times over -- variables,
|
||||
|
||||
@@ -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
|
||||
* 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.
|
||||
*/
|
||||
static int test_node_init_zeroes_the_links(void)
|
||||
@@ -103,7 +103,7 @@ static int test_append_single_node(void)
|
||||
* The defect that made this library's list unusable: `tail` was assigned from
|
||||
* Floyd's `slow` cursor *before* slow advanced, so it tracked the node behind
|
||||
* the midpoint rather than the last node. Appending n1..n4 to n0 produced the
|
||||
* chain "n0 -> n4" and silently dropped n1, n2 and n3. TODO.md 2.1.1.
|
||||
* chain "n0 -> n4" and silently dropped n1, n2 and n3.
|
||||
*/
|
||||
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
|
||||
* its old position and the tail, so the tail walk -- which happens anyway --
|
||||
* doubles as the check.
|
||||
@@ -281,7 +281,7 @@ static int test_iterate_single_node(void)
|
||||
* Every node, exactly once, in order, starting at the head. The cycle check
|
||||
* leaves Floyd's `slow` cursor at the list midpoint, and the visiting loop used
|
||||
* to start from there -- so the whole first half of the list, head included, was
|
||||
* never passed to the callback at all. TODO.md 2.1.2.
|
||||
* never passed to the callback at all.
|
||||
*/
|
||||
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
|
||||
* 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
|
||||
* exhaust the pool several times over, which is where a wrapper that raises an
|
||||
* error and forgets to release it shows up as an outright exhaustion rather than
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
/*
|
||||
* Memory wrappers -- TODO.md section 1.1, now complete, plus the additions from
|
||||
* Memory wrappers, complete, plus the additions from
|
||||
* 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)
|
||||
* returned NULL; an allocation the system cannot satisfy reports ENOMEM and
|
||||
* leaves *dst NULL rather than garbage; and overlapping memcpy is refused with
|
||||
@@ -32,7 +32,7 @@ static int test_malloc_rejects_null_destination(void)
|
||||
}
|
||||
|
||||
/*
|
||||
* 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
|
||||
* need not set errno -- so the old wrapper could raise an error whose status was
|
||||
* 0, which every DETECT downstream reads as success while the context holds a
|
||||
@@ -296,7 +296,7 @@ static int test_memcpy_zero_length_is_noop(void)
|
||||
}
|
||||
|
||||
/*
|
||||
* 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
|
||||
* compiler version or a length changes and it does not". Callers who mean to
|
||||
* overlap want aksl_memmove, and the message says so.
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
/*
|
||||
* aksl_realpath and aksl_realpath_alloc -- 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
|
||||
* hard-coded string, because $TMPDIR may itself be a symlink (/tmp -> /private/tmp
|
||||
* and friends) and the resolved answer is what the platform says it is.
|
||||
*
|
||||
* The failure cases now pass an *uninitialised* resolved_path on purpose. That
|
||||
* used to be the crash case (TODO.md 2.1.6): the wrapper's own error path
|
||||
* used to be the crash case: the wrapper's own error path
|
||||
* formatted the buffer with %s while realpath(3) leaves its contents
|
||||
* unspecified on failure, so the library read uninitialised memory while
|
||||
* reporting an error. The message names only the input path now, and this test
|
||||
@@ -122,7 +122,7 @@ static int test_rejects_null_arguments(void)
|
||||
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_realpath(NULL, resolved, sizeof(resolved)),
|
||||
AKERR_NULLPOINTER, "path=");
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_realpath("/tmp", NULL, PATH_MAX),
|
||||
|
||||
@@ -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:
|
||||
*
|
||||
|
||||
134
tests/test_stat.c
Normal file
134
tests/test_stat.c
Normal 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);
|
||||
}
|
||||
@@ -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
|
||||
* a fixed buffer and no answer at all when the length is not known in advance.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/*
|
||||
* Stream wrappers: aksl_fopen / aksl_fread / aksl_fwrite / aksl_fclose.
|
||||
*
|
||||
* 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
|
||||
* aksl_fread and aksl_fwrite report through nmemb_out, and the three cases that
|
||||
* needed a hostile file to produce: a mode-denied path (EACCES), a full device
|
||||
@@ -81,7 +81,7 @@ static int test_fopen_rejects_null_arguments(void)
|
||||
|
||||
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_fopen(path, "r", NULL),
|
||||
AKERR_NULLPOINTER, "fp=");
|
||||
/* 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),
|
||||
AKERR_NULLPOINTER, "pathname=");
|
||||
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_fopen(path, NULL, &fp),
|
||||
@@ -144,7 +144,7 @@ static int test_fread_short_read_is_eof_and_reports_the_count(void)
|
||||
AKSL_CHECK_OK(aksl_fopen(path, "r", &fp));
|
||||
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_fread(buf, 1, sizeof(buf), fp, &moved),
|
||||
AKERR_EOF, "EOF");
|
||||
/* 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_OK(aksl_fclose(fp));
|
||||
|
||||
@@ -160,7 +160,7 @@ static int test_fread_short_read_is_eof_and_reports_the_count(void)
|
||||
*
|
||||
* That is a change from the old wrapper, which read ferror() and reported
|
||||
* AKERR_IO without ever consulting errno. Routing it through AKSL_ERRNO_OR
|
||||
* (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.
|
||||
*/
|
||||
static int test_fread_from_write_only_stream_reports_errno(void)
|
||||
@@ -194,7 +194,7 @@ static int test_fread_rejects_null_arguments(void)
|
||||
|
||||
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_fread(buf, 1, sizeof(buf), NULL, &moved),
|
||||
AKERR_NULLPOINTER, "fp=");
|
||||
/* 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),
|
||||
AKERR_NULLPOINTER, "ptr=");
|
||||
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_fread(buf, 1, sizeof(buf), fp, NULL),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Stream wrappers beyond open/read/write/close -- src/stream.c, 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
|
||||
* positioning, flushing, character and line I/O, stream state, and formatted
|
||||
|
||||
@@ -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
|
||||
* h = h * 33 + byte for each of len bytes, truncated to 32 bits. They were
|
||||
@@ -7,7 +7,7 @@
|
||||
*
|
||||
* Most vectors here are 7-bit ASCII, where signed and unsigned char agree and
|
||||
* the test therefore says nothing either way about byte signedness. The high-bit
|
||||
* vector is the one that pins TODO.md 2.2.6 down.
|
||||
* vector is the one that pins the sign-extension defect down.
|
||||
*/
|
||||
|
||||
#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
|
||||
* ARM Linux made them negative, giving 5859874 here instead of 5868578 -- a hash
|
||||
* that disagreed with canonical djb2 and, worse, disagreed with itself across
|
||||
@@ -68,7 +68,7 @@ static int test_high_bit_bytes_are_unsigned(void)
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* The NUL-terminated convenience form (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)
|
||||
{
|
||||
const char *s = "libakstdlib";
|
||||
|
||||
@@ -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:
|
||||
* every copying function takes the destination size and treats truncation as an
|
||||
|
||||
@@ -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
|
||||
* akbasic had to hand-write for itself (its src/convert.c, ~60 lines) because
|
||||
|
||||
@@ -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
|
||||
* depth-first orders apart because all three visit all seven nodes -- and could
|
||||
@@ -183,7 +183,7 @@ static int test_dfs_is_an_alias_for_preorder(void)
|
||||
|
||||
/*
|
||||
* BFS was AKERR_NOT_IMPLEMENTED, and the lalloc/lfree parameters that existed to
|
||||
* serve it were defaulted and then never called -- 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.
|
||||
*/
|
||||
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
|
||||
* every allocation is released. The depth-first modes allocate nothing at all,
|
||||
* which is the other half of the contract.
|
||||
@@ -382,7 +382,7 @@ static int test_degenerate_chains(void)
|
||||
}
|
||||
|
||||
/*
|
||||
* 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
|
||||
* documented limit. Built one node past the cap so the failure is the cap itself
|
||||
* and not some incidental shortfall. `static` because AKSL_TREE_MAX_DEPTH nodes
|
||||
@@ -488,7 +488,7 @@ static int test_cyclic_tree_is_caught(void)
|
||||
* that raised the break handled it in its own PROCESS/HANDLE block and returned
|
||||
* success, so the parent frame's PASS saw nothing wrong and carried straight on
|
||||
* into the sibling subtree. All seven nodes were visited no matter where the
|
||||
* break was raised. TODO.md 2.1.3.
|
||||
* break was raised.
|
||||
*
|
||||
* One case per order, each breaking on a node that is *not* last in that order --
|
||||
* which is precisely what the old test could not do, because it hid its target
|
||||
@@ -579,7 +579,7 @@ static int test_null_arguments(void)
|
||||
}
|
||||
|
||||
/*
|
||||
* 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 --
|
||||
* fell straight through to SUCCEED_RETURN having visited nothing at all. A
|
||||
* traversal that silently did not happen, reported as success.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Version reporting -- TODO.md section 2.2.16.
|
||||
* Version reporting.
|
||||
*
|
||||
* There are two versions in play and the whole point of this API is that they
|
||||
* are allowed to differ:
|
||||
|
||||
Reference in New Issue
Block a user