Compare commits
37 Commits
f8425b8729
...
26
| Author | SHA1 | Date | |
|---|---|---|---|
| e763183cc4 | |||
| 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
|
# 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: |
|
||||||
|
|||||||
36
AGENTS.md
36
AGENTS.md
@@ -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,13 +97,19 @@ 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
|
||||||
right list in `CMakeLists.txt`. `AKSL_TESTS` must exit zero.
|
right list in `CMakeLists.txt`. `AKSL_TESTS` must exit zero.
|
||||||
`AKSL_WILL_FAIL_TESTS` are deliberate abort/contract tests.
|
`AKSL_WILL_FAIL_TESTS` are deliberate abort/contract tests.
|
||||||
`AKSL_KNOWN_FAILING_TESTS` assert documented defects from `TODO.md`; when one
|
`AKSL_KNOWN_FAILING_TESTS` assert defects that have an open issue; when one
|
||||||
starts unexpectedly passing, move it into `AKSL_TESTS` with the fix. Both of the
|
starts unexpectedly passing, move it into `AKSL_TESTS` with the fix and close the
|
||||||
|
issue. Both of the
|
||||||
latter are currently empty -- all six confirmed defects are fixed -- but the
|
latter are currently empty -- all six confirmed defects are fixed -- but the
|
||||||
mechanism stays for the next one.
|
mechanism stays for the next one.
|
||||||
|
|
||||||
@@ -119,7 +129,7 @@ file, `find_dependency(akerror)` resolving, and the exported
|
|||||||
Coverage is 99.5% of lines and 100% of functions across all four sources; CI
|
Coverage is 99.5% of lines and 100% of functions across all four sources; CI
|
||||||
gates at 90 (line) / 40 (branch), so new code needs tests in the same commit. Run
|
gates at 90 (line) / 40 (branch), so new code needs tests in the same commit. Run
|
||||||
`cmake --build build-coverage --target coverage` and check the uncovered-line
|
`cmake --build build-coverage --target coverage` and check the uncovered-line
|
||||||
listing before proposing a change. Tests for behaviour that `TODO.md` records as
|
listing before proposing a change. Tests for behaviour an open issue records as
|
||||||
defective belong in `AKSL_KNOWN_FAILING_TESTS` asserting the *correct* contract —
|
defective belong in `AKSL_KNOWN_FAILING_TESTS` asserting the *correct* contract —
|
||||||
do not pin current-but-wrong behaviour in `AKSL_TESTS`, since that turns the
|
do not pin current-but-wrong behaviour in `AKSL_TESTS`, since that turns the
|
||||||
eventual fix into a test failure.
|
eventual fix into a test failure.
|
||||||
@@ -130,10 +140,26 @@ Recent commits use short imperative summaries, for example `Add memory wrapper
|
|||||||
tests` and `Make error-status assertions authoritative`. Keep commits focused
|
tests` and `Make error-status assertions authoritative`. Keep commits focused
|
||||||
and include tests with behavior changes. Pull requests should describe the
|
and include tests with behavior changes. Pull requests should describe the
|
||||||
changed API or behavior, list the CTest/sanitizer/mutation commands run, and
|
changed API or behavior, list the CTest/sanitizer/mutation commands run, and
|
||||||
link the relevant `TODO.md` item or issue when fixing a known defect.
|
link the issue it closes.
|
||||||
|
|
||||||
## Agent-Specific Instructions
|
## Agent-Specific Instructions
|
||||||
|
|
||||||
|
**Outstanding work goes in the issue tracker, not in a file.** Open an issue at
|
||||||
|
<https://source.starfort.tech/andrew/libakstdlib/issues> — `tea issues create
|
||||||
|
--repo andrew/libakstdlib` — naming the file and line, the functional
|
||||||
|
consequence, and what closing it would touch. Label it by kind and blast radius
|
||||||
|
and leave `status::grooming` on it until its scope and approach are settled.
|
||||||
|
**Do not add outstanding items to `TODO.md`**: that file is the record of where
|
||||||
|
the library stands, what is deliberately not wrapped, and which uncovered lines
|
||||||
|
are uncoverable rather than untested. A description of work still to do goes
|
||||||
|
stale the moment somebody does it, which is why the two are separated.
|
||||||
|
|
||||||
|
**A defect in a dependency is filed against that dependency.** `libakerror` has
|
||||||
|
a tracker on the same forge, and three defects that cost this library a
|
||||||
|
workaround each sat in this repository's own notes for months without anybody
|
||||||
|
upstream being able to see them. Comment the workaround at its site with the
|
||||||
|
words "filed upstream" and delete it when the fix lands.
|
||||||
|
|
||||||
Do not modify generated build trees, profiling artifacts, or untracked scratch
|
Do not modify generated build trees, profiling artifacts, or untracked scratch
|
||||||
files unless explicitly asked. Prefer small, test-backed changes and update
|
files unless explicitly asked. Prefer small, test-backed changes and update
|
||||||
`README.md` or `TODO.md` when changing documented workflows or known failures.
|
`README.md` when changing documented workflows.
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -78,7 +78,7 @@ endif()
|
|||||||
# Sanitizer build, off by default:
|
# Sanitizer build, off by default:
|
||||||
# cmake -S . -B build-asan -DAKSL_SANITIZE=ON && ctest --test-dir build-asan
|
# cmake -S . -B build-asan -DAKSL_SANITIZE=ON && ctest --test-dir build-asan
|
||||||
# Set before the dependency is added so libakerror is instrumented too --
|
# Set before the dependency is added so libakerror is instrumented too --
|
||||||
# several of the defects in TODO.md section 2 (the uninitialised %s in
|
# several of the defects listed in UPGRADING.md (the uninitialised %s in
|
||||||
# aksl_realpath, the unbounded vsprintf in aksl_sprintf, the missing va_end in
|
# aksl_realpath, the unbounded vsprintf in aksl_sprintf, the missing va_end in
|
||||||
# the printf family) only show up under ASan/UBSan.
|
# the printf family) only show up under ASan/UBSan.
|
||||||
option(AKSL_SANITIZE "Build the library and its tests with ASan + UBSan" OFF)
|
option(AKSL_SANITIZE "Build the library and its tests with ASan + UBSan" OFF)
|
||||||
@@ -173,7 +173,7 @@ if(CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR)
|
|||||||
# configure at all. Rename the dependency's on the way past rather than
|
# 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
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -298,7 +299,7 @@ install(FILES
|
|||||||
# reaching FINISH_NORETURN, a deliberate contract
|
# reaching FINISH_NORETURN, a deliberate contract
|
||||||
# violation), so a non-zero exit is a pass.
|
# violation), so a non-zero exit is a pass.
|
||||||
# AKSL_KNOWN_FAILING_TESTS assert the *correct* behaviour of a confirmed
|
# AKSL_KNOWN_FAILING_TESTS assert the *correct* behaviour of a confirmed
|
||||||
# defect from TODO.md section 2.1. They fail until
|
# defect; see UPGRADING.md. They fail until
|
||||||
# the defect is fixed, and are marked WILL_FAIL so
|
# the defect is fixed, and are marked WILL_FAIL so
|
||||||
# the suite stays green and the gap stays visible.
|
# the suite stays green and the gap stays visible.
|
||||||
# When one is fixed CTest reports it as failed with
|
# When one is fixed CTest reports it as failed with
|
||||||
@@ -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
|
||||||
|
|||||||
55
README.md
55
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.
|
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
|
||||||
@@ -56,7 +57,7 @@ error reporting is not.
|
|||||||
|
|
||||||
There is no TSan test here because there is nothing to verify — the answer is
|
There is no TSan test here because there is nothing to verify — the answer is
|
||||||
known and it is "no". Fixing it means locking or thread-local storage in
|
known and it is "no". Fixing it means locking or thread-local storage in
|
||||||
libakerror's pool, which is that library's decision to make; `TODO.md` §1.9
|
libakerror's pool, which is that library's decision to make; issue #2
|
||||||
records it. Until then: confine libakstdlib calls to one thread, or serialise
|
records it. Until then: confine libakstdlib calls to one thread, or serialise
|
||||||
them yourself.
|
them yourself.
|
||||||
|
|
||||||
@@ -100,7 +101,7 @@ copy in your build directory; change the template or `project()`.
|
|||||||
It is `0.x` deliberately. The 0.1 → 0.2 bump was itself an ABI break — fixing the
|
It is `0.x` deliberately. The 0.1 → 0.2 bump was itself an ABI break — fixing the
|
||||||
confirmed defects changed five signatures and the `ato*` contract, all of it
|
confirmed defects changed five signatures and the `ato*` contract, all of it
|
||||||
listed in `UPGRADING.md` — and the API is not being promised until the wishlist
|
listed in `UPGRADING.md` — and the API is not being promised until the wishlist
|
||||||
in `TODO.md` §3 has settled. While the major version is `0`, **the soname carries
|
in the tracker has settled. While the major version is `0`, **the soname carries
|
||||||
`MAJOR.MINOR`**: 0.1 and 0.2
|
`MAJOR.MINOR`**: 0.1 and 0.2
|
||||||
are different ABIs and the loader will not substitute one for the other. At 1.0
|
are different ABIs and the loader will not substitute one for the other. At 1.0
|
||||||
the soname becomes `MAJOR` alone — the `if(PROJECT_VERSION_MAJOR EQUAL 0)` in
|
the soname becomes `MAJOR` alone — the `if(PROJECT_VERSION_MAJOR EQUAL 0)` in
|
||||||
@@ -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
|
||||||
|
|
||||||
@@ -225,10 +227,10 @@ of them invert the meaning of "Passed":
|
|||||||
|---|---|
|
|---|---|
|
||||||
| `AKSL_TESTS` | Ordinary tests. Must exit 0. |
|
| `AKSL_TESTS` | Ordinary tests. Must exit 0. |
|
||||||
| `AKSL_WILL_FAIL_TESTS` | Expected to abort by design — an unhandled error reaching `FINISH_NORETURN`, or a deliberate contract violation. Marked `WILL_FAIL`, so a non-zero exit is a pass. |
|
| `AKSL_WILL_FAIL_TESTS` | Expected to abort by design — an unhandled error reaching `FINISH_NORETURN`, or a deliberate contract violation. Marked `WILL_FAIL`, so a non-zero exit is a pass. |
|
||||||
| `AKSL_KNOWN_FAILING_TESTS` | Assert the *correct* behaviour of a confirmed defect (see `TODO.md` §2.1). Also marked `WILL_FAIL`. |
|
| `AKSL_KNOWN_FAILING_TESTS` | Assert the *correct* behaviour of a confirmed defect (see `UPGRADING.md`). Also marked `WILL_FAIL`. |
|
||||||
|
|
||||||
**Both of those lists are currently empty**, which is the news: all six confirmed
|
**Both of those lists are currently empty**, which is the news: all six confirmed
|
||||||
defects in `TODO.md` §2.1 are fixed, and the four tests that used to sit in
|
defects recorded in `UPGRADING.md` are fixed, and the four tests that used to sit in
|
||||||
`AKSL_KNOWN_FAILING_TESTS` are folded back into the tests for the things they
|
`AKSL_KNOWN_FAILING_TESTS` are folded back into the tests for the things they
|
||||||
test, where they now have to keep passing rather than keep failing visibly. The
|
test, where they now have to keep passing rather than keep failing visibly. The
|
||||||
mechanism stays for the next one. When a defect is fixed its known-failing test
|
mechanism stays for the next one. When a defect is fixed its known-failing test
|
||||||
@@ -322,7 +324,7 @@ so a top-level `-DAKSL_COVERAGE=ON` build would collide on the name and fail to
|
|||||||
configure at all. `CMakeLists.txt` renames the dependency's to `akerror_coverage`
|
configure at all. `CMakeLists.txt` renames the dependency's to `akerror_coverage`
|
||||||
on the way past — it drives its own instrumented build tree, so
|
on the way past — it drives its own instrumented build tree, so
|
||||||
`cmake --build build-coverage --target akerror_coverage` still works. The
|
`cmake --build build-coverage --target akerror_coverage` still works. The
|
||||||
workaround goes away when libakerror namespaces it upstream; see `TODO.md` §2.3.
|
workaround goes away when libakerror namespaces it upstream; see issue #4.
|
||||||
|
|
||||||
CTest hides the output of a passing test, so `coverage_report` also writes
|
CTest hides the output of a passing test, so `coverage_report` also writes
|
||||||
`build-coverage/coverage-summary.txt` (the same text report) and
|
`build-coverage/coverage-summary.txt` (the same text report) and
|
||||||
@@ -379,7 +381,7 @@ lines are uncovered and each is uncovered on purpose:
|
|||||||
- **Two in `aksl_fread`/`aksl_fwrite`**, the short transfer with *neither* `feof`
|
- **Two in `aksl_fread`/`aksl_fwrite`**, the short transfer with *neither* `feof`
|
||||||
nor `ferror` set. Every way of producing a short transfer on Linux sets one or
|
nor `ferror` set. Every way of producing a short transfer on Linux sets one or
|
||||||
the other; the branch is there because the standard permits neither, not
|
the other; the branch is there because the standard permits neither, not
|
||||||
because anything reaches it. `TODO.md` §1.2 records it as still open.
|
because anything reaches it. Issue #6 records it as still open.
|
||||||
|
|
||||||
Branch coverage sits far below line coverage because most branches in these files
|
Branch coverage sits far below line coverage because most branches in these files
|
||||||
are inside the `FAIL_*`/`ATTEMPT`/`FINISH` macro expansions — pool exhaustion,
|
are inside the `FAIL_*`/`ATTEMPT`/`FINISH` macro expansions — pool exhaustion,
|
||||||
@@ -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 313/7 # 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
|
||||||
|
|||||||
523
TODO.md
523
TODO.md
@@ -1,11 +1,16 @@
|
|||||||
# TODO
|
# Record
|
||||||
|
|
||||||
Working notes for `libakstdlib`. **Outstanding items only** — anything fixed comes
|
**Outstanding work is in the issue tracker, not in this file:**
|
||||||
out of this file and goes into `UPGRADING.md`, the tests, or a comment beside the
|
<https://source.starfort.tech/andrew/libakstdlib/issues>
|
||||||
code, whichever is the right place to be reminded of it.
|
|
||||||
|
|
||||||
Ordered by blast radius: what blocks other work first, what is merely wrong
|
What stays here is what a tracker has no place for: where the library stands, and
|
||||||
second, what is missing last.
|
the decisions that would otherwise be re-litigated — what is deliberately *not*
|
||||||
|
wrapped, and which uncovered lines are uncoverable rather than untested.
|
||||||
|
|
||||||
|
Issues are labelled by kind and blast radius, and milestoned by what they can land
|
||||||
|
in: `0.2.x` for anything that breaks no ABI, `0.3.0` for new public symbols,
|
||||||
|
`1.0.0` for the large surfaces. Everything filed carries `status::grooming` until
|
||||||
|
it has been through grooming.
|
||||||
|
|
||||||
## Where the library stands
|
## Where the library stands
|
||||||
|
|
||||||
@@ -17,170 +22,64 @@ second, what is missing last.
|
|||||||
| 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 313 times and raw libc 7 — **2.2% 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
|
`AKSL_KNOWN_FAILING_TESTS` is empty. What they were, and what changed as a result,
|
||||||
result, is in `UPGRADING.md`.
|
is in `UPGRADING.md`.
|
||||||
|
|
||||||
---
|
## What libakerror costs this library
|
||||||
|
|
||||||
## 1. Blocked on libakerror
|
Three of these are not fixable from inside this repository, and each costs
|
||||||
|
something here. They are filed in both places, because the fix is there and the
|
||||||
|
bill is here. The fourth turned out not to be blocked at all:
|
||||||
|
|
||||||
These are not fixable from inside this repository. Each one currently costs
|
| Here | Upstream | What it costs |
|
||||||
something here, and the cost is what makes them worth carrying.
|
|---|---|---|
|
||||||
|
| #2 | — | **Corrected while filing.** The unlocked error pool is a property of the libakerror this repository *pins* (1.0.0), not of libakerror (2.0.1, which locks it). `libakgl` and `akbasic` are both on 2.0.1. The work is a submodule bump and the verification that goes with it, not a wait |
|
||||||
|
| #3 | libakerror | `IGNORE()` leaks a context, so `aksl_tree_iterate` open-codes log-then-release in four lines that should be one |
|
||||||
|
| #4 | libakerror | The `coverage` target is not namespaced when embedded, so `-DAKSL_COVERAGE=ON` fails to configure and `CMakeLists.txt` shadows `add_custom_target` to work around it |
|
||||||
|
| #5 | libakerror | No `akerrorConfigVersion.cmake`, so `find_dependency(akerror)` cannot ask for the 1.0.0 floor |
|
||||||
|
|
||||||
### 1.1 The error pool is a process-global array with no locking
|
## Uncovered lines that are uncoverable
|
||||||
|
|
||||||
**This is what makes the library single-threaded**, and it is the largest open
|
Eight lines, and this is what they are, so the coverage listing does not read as an
|
||||||
item by some distance.
|
oversight.
|
||||||
|
|
||||||
`AKERR_ARRAY_ERROR` is a fixed array in `deps/libakerror/src/error.c`, handed out
|
**Two are the short-transfer branch** in `aksl_fread`/`aksl_fwrite` — a short
|
||||||
by `akerr_next_error()` with no synchronisation of any kind. Every entry point in
|
transfer with neither EOF nor a stream error. The standard permits it, so the
|
||||||
this library takes a slot from it on any failure path, so two threads raising
|
branch is correct to have; every way of actually producing one on Linux sets `feof`
|
||||||
errors concurrently can be handed the same slot and will corrupt each other's
|
or `ferror` first. **It is the only error path in the library that has never
|
||||||
message, status and stack trace.
|
executed**, and reaching it needs a `FILE *` over a custom stream (`fopencookie`,
|
||||||
|
`funopen`). That is #6.
|
||||||
|
|
||||||
**Consequence.** `README.md` says plainly that the library is not thread-safe.
|
**Two are the string-buffer overflow guard** — the `capacity = needed` arm in
|
||||||
That is honest, and it is also a hard ceiling: §4's `pthread_*` and socket
|
`strbuf_reserve`. Reaching it needs an `aksl_StrBuf` within a factor of two of
|
||||||
wrappers cannot be written until this is resolved, because a threading API
|
`SIZE_MAX`, **which is not a test, it is a hang.** It is there because doubling a
|
||||||
nobody can call from a thread is not an API.
|
capacity is a multiplication, and an unguarded one is how a growable buffer turns
|
||||||
|
into a heap overflow. Nothing to do.
|
||||||
|
|
||||||
**What closing it would touch.** libakerror's pool — either a mutex around
|
**Four are `HANDLE(e, AKERR_ITERATOR_BREAK)` lines**, and they are macro artifacts
|
||||||
`akerr_next_error`/`akerr_release_error`, or thread-local slot arrays, which
|
rather than gaps. In libakerror that macro begins with the `break;` belonging to
|
||||||
would suit the bounded-preallocation style better and cost nothing on the
|
`PROCESS`'s `case 0:` arm, reachable only when a callback returns a non-NULL context
|
||||||
single-threaded path. Then a TSan job in `.gitea/workflows/ci.yaml` and a
|
whose status is *zero* — the pathological case the errno-fallback work removed. Left
|
||||||
concurrent smoke test here, and the warning in `README.md` comes out.
|
uncovered deliberately rather than pinned by a test that would have to manufacture
|
||||||
|
it.
|
||||||
|
|
||||||
### 1.2 `IGNORE()` logs a context and never releases it
|
## `aksl_version_check()` ignores its `patch` argument
|
||||||
|
|
||||||
`deps/libakerror/include/akerror.tmpl.h:308`. The macro assigns the context to
|
`src/stdlib.c`, the `(void)patch`. **Correct for the current "same soname" rule** —
|
||||||
`__akerr_last_ignored`, logs it, and stops. The slot is never returned to the
|
|
||||||
pool, so every `IGNORE()` on a failing call leaks one — and after
|
|
||||||
`AKERR_MAX_ARRAY_ERROR` of them the pool is exhausted and `ENSURE_ERROR_READY`
|
|
||||||
calls `exit(1)`.
|
|
||||||
|
|
||||||
**Consequence here.** `aksl_tree_iterate`'s `CLEANUP` block (`src/stdlib.c`)
|
|
||||||
cannot use `IGNORE()` to drop a queue-drain failure and open-codes the
|
|
||||||
log-then-release by hand instead, with a comment saying why. It is four lines
|
|
||||||
that should be one.
|
|
||||||
|
|
||||||
**What closing it would touch.** One `RELEASE_ERROR` in the macro; then delete
|
|
||||||
the workaround here.
|
|
||||||
|
|
||||||
### 1.3 libakerror does not namespace its `coverage` target when embedded
|
|
||||||
|
|
||||||
`deps/libakerror/CMakeLists.txt:172` versus `:189` — it namespaces `mutation` and
|
|
||||||
not `coverage`.
|
|
||||||
|
|
||||||
**Consequence here.** A `-DAKSL_COVERAGE=ON` top-level build fails to configure
|
|
||||||
at all: *"another target with the same name already exists"*. `CMakeLists.txt`
|
|
||||||
shadows `add_custom_target` for the duration of the `add_subdirectory()` call and
|
|
||||||
renames the dependency's to `akerror_coverage`.
|
|
||||||
|
|
||||||
**What closing it would touch.** Apply the same
|
|
||||||
`CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR` test upstream that the
|
|
||||||
`mutation` target already has; then delete the shadow here, which sits directly
|
|
||||||
above the `add_test` shadow and shares its comment.
|
|
||||||
|
|
||||||
### 1.4 libakerror installs no `akerrorConfigVersion.cmake`
|
|
||||||
|
|
||||||
**Consequence here.** `cmake/akstdlib.cmake.in` has to call
|
|
||||||
`find_dependency(akerror)` with no version, because a request for one would be
|
|
||||||
refused for want of a version file no matter what is installed. The 1.0.0 floor
|
|
||||||
therefore rests on `akstdlib.pc`'s `Requires:` and the `#error` guard in
|
|
||||||
`akstdlib.h`, neither of which covers a `find_package` consumer.
|
|
||||||
|
|
||||||
**What closing it would touch.** One `write_basic_package_version_file()` call
|
|
||||||
upstream — libakstdlib already does this correctly and can be copied — then add
|
|
||||||
the `1.0.0` floor to the `find_dependency` here.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. Known gaps in what is already wrapped
|
|
||||||
|
|
||||||
### 2.1 A short transfer with neither EOF nor a stream error is untested
|
|
||||||
|
|
||||||
`src/stdlib.c`, the `FAIL_RETURN(e, AKERR_IO, "short read: ...")` in `aksl_fread`
|
|
||||||
and its counterpart in `aksl_fwrite`. Two of the eight uncovered lines in the
|
|
||||||
whole library.
|
|
||||||
|
|
||||||
The standard permits a short transfer with neither indicator set, so the branch
|
|
||||||
is correct to have. Every way of actually producing one on Linux sets `feof` or
|
|
||||||
`ferror` first, so nothing in the suite reaches it.
|
|
||||||
|
|
||||||
**Consequence.** Low: the code is a handful of lines and reviewed, but it is the
|
|
||||||
only error path in the library that has never executed.
|
|
||||||
|
|
||||||
**What closing it would touch.** A `FILE *` over a custom stream — `fopencookie`
|
|
||||||
on glibc, `funopen` on the BSDs — whose read function returns a short count
|
|
||||||
without setting either flag. That is a platform-specific test helper in
|
|
||||||
`tests/aksl_capture.h` guarded on the platform, which is why it has not been
|
|
||||||
written yet rather than an oversight.
|
|
||||||
|
|
||||||
### 2.2 The string-buffer overflow guard is untestable
|
|
||||||
|
|
||||||
`src/collections.c`, the `capacity = needed` arm in `strbuf_reserve`. Reaching it
|
|
||||||
needs an `aksl_StrBuf` within a factor of two of `SIZE_MAX`, which is not a test,
|
|
||||||
it is a hang. The other two uncovered lines.
|
|
||||||
|
|
||||||
**Consequence.** None known. It is there because doubling a capacity is a
|
|
||||||
multiplication, and an unguarded one is how a growable buffer turns into a heap
|
|
||||||
overflow.
|
|
||||||
|
|
||||||
**What closing it would touch.** Nothing worth doing. Recorded so the coverage
|
|
||||||
listing does not read as an oversight.
|
|
||||||
|
|
||||||
### 2.3 `aksl_version_check()` ignores its `patch` argument
|
|
||||||
|
|
||||||
`src/stdlib.c`, the `(void)patch`. Correct for the current "same soname" rule —
|
|
||||||
patch level never breaks the ABI — but the parameter exists only so the error
|
patch level never breaks the ABI — but the parameter exists only so the error
|
||||||
message can name the caller's full version.
|
message can name the caller's full version.
|
||||||
|
|
||||||
**Consequence.** None today. If a future compatibility rule needs the patch level
|
No consequence today. If a future compatibility rule needs the patch level to
|
||||||
to participate, that is the line to change, and the `#if` in
|
participate, that is the line to change, and the `#if` in `tests/test_version.c` is
|
||||||
`tests/test_version.c` is the test that encodes the rule.
|
the test that encodes the rule.
|
||||||
|
|
||||||
### 2.4 Surviving mutants worth turning into assertions
|
## Deliberate omissions
|
||||||
|
|
||||||
The mutation harness samples 260 of 1701 mutants and kills 72.3% of them. Most of
|
Recorded so nobody adds them thinking they were forgotten. **Each is a decision,
|
||||||
the 72 survivors are equivalent mutants rather than missing tests — `README.md`
|
and each can be revisited with an argument.**
|
||||||
has the full breakdown — but three clusters are real work:
|
|
||||||
|
|
||||||
- **`FINISH(e, true)` → `FINISH(e, false)`, 3 survivors.** An error swallowed
|
|
||||||
instead of propagated out of an `ATTEMPT` block, and nothing notices. Each one
|
|
||||||
is a call whose failure path is exercised but whose *propagation* is not: the
|
|
||||||
test asserts the status the callee raised without checking it came from the
|
|
||||||
callee rather than being re-raised locally. `tests/test_pool.c`'s origin
|
|
||||||
assertions are the shape of the fix.
|
|
||||||
- **`SUCCEED_RETURN` deleted, 7 survivors.** The function falls off the end and
|
|
||||||
returns whatever is in the return register, which is NULL often enough to pass.
|
|
||||||
These need an assertion on the *side effect* — the buffer that was filled, the
|
|
||||||
node that was linked — rather than on the returned status.
|
|
||||||
- **`FAIL_*` guards deleted, ~6 of the 14 in that group.** Each is an argument
|
|
||||||
check nothing drives. The other 8 in the group are constant shifts that no test
|
|
||||||
can catch, because the test names the same constant symbolically and moves with
|
|
||||||
it.
|
|
||||||
|
|
||||||
Two survivors in this class were real and are fixed: the right child's
|
|
||||||
`depth + 1` in the depth-first walk, and `aksl_tree_remove` on an empty tree.
|
|
||||||
|
|
||||||
**What closing them would touch.** Only `tests/`. Raise the `--threshold` in
|
|
||||||
`.gitea/workflows/ci.yaml` and `.githooks/pre-push` in step, as a ratchet.
|
|
||||||
|
|
||||||
### 2.5 Four uncovered `HANDLE(e, AKERR_ITERATOR_BREAK)` lines
|
|
||||||
|
|
||||||
Macro artifacts rather than gaps. In libakerror that macro begins with the
|
|
||||||
`break;` belonging to `PROCESS`'s `case 0:` arm, reachable only when a callback
|
|
||||||
returns a non-NULL context whose status is *zero* — which is the pathological case
|
|
||||||
the errno-fallback work removed. Left uncovered deliberately rather than pinned
|
|
||||||
by a test that would have to manufacture it.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. Deliberate omissions
|
|
||||||
|
|
||||||
Recorded so nobody adds them thinking they were forgotten. Each is a decision,
|
|
||||||
and each can be revisited with an argument.
|
|
||||||
|
|
||||||
| Not wrapped | Why |
|
| Not wrapped | Why |
|
||||||
|---|---|
|
|---|---|
|
||||||
@@ -191,131 +90,29 @@ and each can be revisited with an argument.
|
|||||||
| `perror` | Writes to stderr and consults a global. `aksl_strerror` is the akerror-native equivalent and knows this library's own statuses as well as errno's. |
|
| `perror` | Writes to stderr and consults a global. `aksl_strerror` is the akerror-native equivalent and knows this library's own statuses as well as errno's. |
|
||||||
| `strerror_r` | Two incompatible functions share that name and which one you get depends on feature-test macros a consumer cannot influence from inside this header. `aksl_strerror` is built on libakerror's registry instead. |
|
| `strerror_r` | Two incompatible functions share that name and which one you get depends on feature-test macros a consumer cannot influence from inside this header. `aksl_strerror` is built on libakerror's registry instead. |
|
||||||
|
|
||||||
---
|
## What the mutation survivors mean
|
||||||
|
|
||||||
## 4. Not yet wrapped
|
The harness samples 260 of 1701 mutants and kills 72.3%. **Most of the 72 survivors
|
||||||
|
are equivalent mutants rather than missing tests** — `README.md` has the full
|
||||||
|
breakdown — and knowing which is which is the point, because a ratchet built on the
|
||||||
|
wrong number is a ratchet that stops moving.
|
||||||
|
|
||||||
Ordered by how much a caller of this library would miss them. §3.1 and §3.6 of
|
Three clusters are real work and are #7. Two survivors in that class were real and
|
||||||
the old numbering are done; what follows is what is left.
|
are fixed: the right child's `depth + 1` in the depth-first walk, and
|
||||||
|
`aksl_tree_remove` on an empty tree.
|
||||||
|
|
||||||
### 4.1 POSIX file and process API
|
## Evidence from the first full consumer
|
||||||
|
|
||||||
The most likely next surface. Nothing here is blocked; it is simply not written.
|
`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,
|
||||||
**`unistd.h` / `fcntl.h`**
|
20,169 now. It was the first consumer to exercise the whole surface rather than a
|
||||||
- [ ] `open`, `close`, `read`, `write`, `pread`, `pwrite`, `lseek`
|
corner of it, **and what it could not use is what prioritised everything that has
|
||||||
- [ ] `readv`, `writev`
|
been built since.**
|
||||||
- [ ] `dup`, `dup2`, `pipe`, `fcntl`
|
|
||||||
- [ ] `fsync`, `fdatasync`, `truncate`, `ftruncate`
|
|
||||||
- [ ] `unlink`, `link`, `symlink`, `readlink`, `rmdir`, `mkdir`
|
|
||||||
- [ ] `access`, `faccessat`, `chmod`, `fchmod`, `chown`, `fchown`, `umask`
|
|
||||||
- [ ] `chdir`, `fchdir`, `getcwd`
|
|
||||||
- [ ] `isatty`, `ttyname_r`
|
|
||||||
- [ ] `sysconf`, `pathconf`
|
|
||||||
- [ ] `sleep`, `usleep`, `nanosleep`
|
|
||||||
|
|
||||||
The short-read/short-write contract is the interesting part: `read(2)` returning
|
|
||||||
fewer bytes than asked for is *normal* on a pipe or a socket and a failure on a
|
|
||||||
regular file, so the wrapper needs the same transferred-count out-param
|
|
||||||
`aksl_fread` has, and callers need to be told which case they are in.
|
|
||||||
|
|
||||||
**`sys/stat.h`**
|
|
||||||
- [ ] `stat`, `fstat`, `lstat`, `fstatat`
|
|
||||||
- [ ] `statvfs`, `fstatvfs`
|
|
||||||
|
|
||||||
**`dirent.h`**
|
|
||||||
- [ ] `opendir`, `fdopendir`, `readdir`, `closedir`, `rewinddir`, `scandir`
|
|
||||||
|
|
||||||
`readdir(3)` returning NULL for both "end of directory" and "error, check errno"
|
|
||||||
is the same conflation `aksl_fgetc` and `aksl_fgets` already untangle, and should
|
|
||||||
be untangled the same way: AKERR_EOF for the end, the errno for the error.
|
|
||||||
|
|
||||||
**Process control**
|
|
||||||
- [ ] `fork`, the `exec*` family, `waitpid`, `wait`
|
|
||||||
- [ ] `posix_spawn`
|
|
||||||
- [ ] `system`, `popen`, `pclose`
|
|
||||||
- [ ] `getpid`, `getppid`, `getuid`, `geteuid`, `setuid`, `setgid`
|
|
||||||
- [ ] `atexit`, `exit`, `_exit`, `abort` — mostly to give akerror a shutdown hook
|
|
||||||
- [ ] `getenv`, `setenv`, `unsetenv`, `putenv`, `clearenv`
|
|
||||||
|
|
||||||
`fork` needs thinking about before it is wrapped: the error pool is inherited by
|
|
||||||
the child, and any context live at the moment of the fork exists twice
|
|
||||||
afterwards.
|
|
||||||
|
|
||||||
**`sys/mman.h`**
|
|
||||||
- [ ] `mmap`, `munmap`, `mprotect`, `msync`, `madvise`
|
|
||||||
|
|
||||||
### 4.2 Time
|
|
||||||
|
|
||||||
- [ ] `time`, `clock_gettime`, `clock_getres`, `gettimeofday`
|
|
||||||
- [ ] `localtime_r`, `gmtime_r`, `mktime`, `timegm`, `difftime`
|
|
||||||
- [ ] `strftime`, `strptime`
|
|
||||||
- [ ] `clock`, `times`
|
|
||||||
|
|
||||||
`strftime(3)` returns 0 for both "the output was empty" and "it did not fit",
|
|
||||||
which is the bounded-write ambiguity `aksl_snprintf` already resolves.
|
|
||||||
|
|
||||||
### 4.3 Sorting, searching and the rest of `stdlib.h`
|
|
||||||
|
|
||||||
- [ ] `qsort`, `qsort_r`, `bsearch` — a comparator that fails currently has
|
|
||||||
nowhere to put the error. An akerror-aware comparator signature, shaped
|
|
||||||
like the `aksl_TreeCompareFunc` the tree functions already take, would be a
|
|
||||||
genuine improvement over libc rather than a wrapper around it.
|
|
||||||
- [ ] `abs`, `labs`, `llabs`, `div`, `ldiv`, `lldiv` — note that
|
|
||||||
`abs(INT_MIN)` is undefined behaviour, which is exactly the kind of silent
|
|
||||||
trap worth surfacing.
|
|
||||||
- [ ] `rand`, `srand`, `random`, `srandom`, `getrandom`/`arc4random`
|
|
||||||
|
|
||||||
### 4.4 Larger surfaces
|
|
||||||
|
|
||||||
- [ ] **Sockets**: `socket`, `bind`, `listen`, `accept`, `connect`, the
|
|
||||||
`send`/`recv` families, `shutdown`, `setsockopt`/`getsockopt`,
|
|
||||||
`getaddrinfo`/`freeaddrinfo`/`gai_strerror`, `inet_ntop`/`inet_pton`.
|
|
||||||
`getaddrinfo` is the interesting one: it has its own error space, neither
|
|
||||||
errno nor akerror, which needs mapping into a registered status range.
|
|
||||||
- [ ] **Multiplexing**: `select`, `poll`, `ppoll`, `epoll_*`
|
|
||||||
- [ ] **Signals**: `sigaction`, `sigprocmask`, `sigemptyset`/`sigaddset`, `kill`,
|
|
||||||
`raise`, `signalfd`. A handler cannot raise an akerror context — the pool is
|
|
||||||
not async-signal-safe — so the wrapper covers installation and masking only,
|
|
||||||
and that limit should be documented rather than discovered.
|
|
||||||
- [ ] **Threads**: `pthread_create`/`join`/`detach`, `pthread_mutex_*`,
|
|
||||||
`pthread_cond_*`, `pthread_rwlock_*`, `sem_*`. **Blocked on §1.1.**
|
|
||||||
- [ ] **Dynamic loading**: `dlopen`, `dlsym`, `dlclose`, `dlerror`. `dlerror(3)`
|
|
||||||
is the same "returns a string and clears itself" trap as `strerror`.
|
|
||||||
- [ ] **Locale / wide chars**: `setlocale`, `mbstowcs`, `wcstombs`, `iconv_*`
|
|
||||||
- [ ] **Math**: the `math.h` functions that set `errno` or raise FP exceptions
|
|
||||||
(`sqrt`, `log`, `pow`, `acos`, …). Probably a dedicated `libakmath` rather
|
|
||||||
than more surface here — the failure model is `fetestexcept`, not errno, and
|
|
||||||
it does not resemble anything else in this library.
|
|
||||||
|
|
||||||
### 4.5 Data structures
|
|
||||||
|
|
||||||
The list and tree API is complete against what §3.6 asked for. What a second
|
|
||||||
consumer might want next:
|
|
||||||
|
|
||||||
- [ ] A dynamic array / vector, on the same "caller owns the storage" terms as
|
|
||||||
`aksl_HashMap`.
|
|
||||||
- [ ] A hash map keyed on something other than a string — the current one copies
|
|
||||||
keys into fixed slots, which is right for identifiers and wrong for
|
|
||||||
anything large or binary.
|
|
||||||
- [ ] Balanced insertion for the tree. It is a plain unbalanced BST and says so;
|
|
||||||
sorted input gives a degenerate chain that `AKSL_TREE_MAX_DEPTH` then
|
|
||||||
refuses. Bounded rather than dangerous, but a red-black or AVL variant is
|
|
||||||
the honest fix if anyone inserts sorted data in earnest.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. Evidence from the first full consumer
|
|
||||||
|
|
||||||
`akbasic` (`source.starfort.tech/andrew/akbasic`) is a ~6,300-line C interpreter
|
|
||||||
built on this library and `libakerror`. It was the first consumer to exercise the
|
|
||||||
whole surface rather than a corner of it, and what it *could not* use is what
|
|
||||||
prioritised the work above.
|
|
||||||
|
|
||||||
**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 |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
@@ -326,32 +123,170 @@ 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).
|
1. **A strict `strtoll`/`strtod` wrapper** (`akbasic/src/convert.c`, ~60 lines). The
|
||||||
The `aksl_strto*` family is that, with the endptr/`errno`/range contract.
|
`aksl_strto*` family is that, with the endptr/`errno`/range contract. akbasic
|
||||||
akbasic's own `TODO.md` §1.9 formally bans the `aksl_ato*` family because
|
formally banned the `aksl_ato*` family because routing four diagnosable errors
|
||||||
routing four diagnosable errors through it would have turned them into wrong
|
through it would have turned them into wrong answers — `VAL("garbage")` silently
|
||||||
answers — `VAL("garbage")` silently returning `0.0`. That ban can be lifted:
|
returning `0.0`. **That ban can be lifted**: the `ato*` forms report failures now.
|
||||||
the `ato*` forms report failures now. **`src/convert.c` can be deleted.**
|
2. **A fixed-capacity string-keyed hash table** (`akbasic/src/symtab.c`, ~130 lines,
|
||||||
2. **A fixed-capacity string-keyed hash table** (`akbasic/src/symtab.c`, ~130
|
needed three times over). `aksl_hashmap_*` is that table generalised, **with
|
||||||
lines, needed three times over). `aksl_hashmap_*` is that table generalised,
|
tombstones on delete, which the original did not have.**
|
||||||
with tombstones on delete, which the original did not have.
|
3. **The bounded-copy-with-truncation-as-error idiom, at ten sites.** `aksl_strcpy`
|
||||||
3. **The bounded-copy-with-truncation-as-error idiom, at ten sites.**
|
and `aksl_strncpy` are exactly that idiom.
|
||||||
`aksl_strcpy` and `aksl_strncpy` are exactly that idiom.
|
4. **Uppercase folding for case-insensitive lookup, three times.** `aksl_strcasecmp`
|
||||||
4. **Uppercase folding for case-insensitive lookup, three times.**
|
and `aksl_strncasecmp`.
|
||||||
`aksl_strcasecmp` and `aksl_strncasecmp`.
|
|
||||||
|
|
||||||
**And the four confirmed-with-impact items are closed.** §2.2.4's unbounded
|
**And the four confirmed-with-impact defects are closed.** The unbounded
|
||||||
`aksl_sprintf` is gone; §2.2.2's unchecked `aksl_fopen` arguments are checked, so
|
`aksl_sprintf` is gone; `aksl_fopen`'s arguments are checked, so
|
||||||
`akbasic_cmd_dload`'s hand-rolled validation and its comment pointing here can go;
|
`akbasic_cmd_dload`'s hand-rolled validation and its comment pointing here can go;
|
||||||
§2.2.6's sign-extended djb2 reads bytes unsigned; and §2.1.4's missing `va_end` —
|
the sign-extended djb2 reads bytes unsigned; and the missing `va_end` — which
|
||||||
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
|
### The recount, against this release
|
||||||
lists and no trees, drawing everything from fixed pools by design. A consumer that
|
|
||||||
does allocate would weight §4.1's `open`/`read`/`write` far higher than this one
|
akbasic's `src/` was ported onto 0.2.0 and counted again (#26). The port builds
|
||||||
does. The next thing worth doing is porting akbasic onto this release and
|
clean at `-Wall -Wextra`, passes **112/112** of akbasic's ctest suite, and is
|
||||||
counting the calls again.
|
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** | **313** | **7** | **2.2%** |
|
||||||
|
|
||||||
|
**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% → 2.2% 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. 278 of the 285 sites converted; the 7 that did not are
|
||||||
|
blocked by wrapper *shape*, and they are the useful output:
|
||||||
|
|
||||||
|
| Why it could not be used | Sites | Where |
|
||||||
|
|---|---|---|
|
||||||
|
| Truncation is the answer, not the error | 6 | `runtime.c` `akbasic_runtime_error`, `host.c` (×3), `runtime_struct.c` (×2) |
|
||||||
|
| A `bsearch(3)` comparator, whose signature libc fixes, so there is no out parameter to report through | 1 | `verbs.c` `verb_compare` |
|
||||||
|
|
||||||
|
**The eight sites that used to head this table are gone, and how they went is the
|
||||||
|
finding.** They were the `bool` predicates and `void` helpers with no error channel
|
||||||
|
to route into, and the first filing (#38) asked this library for a form they could
|
||||||
|
call. Andrew ruled that invalid: a function that cannot report an error changes its
|
||||||
|
own signature rather than being handed a way to swallow one. Seven of the eight did
|
||||||
|
exactly that — they now return `akerr_ErrorContext *` and hand the answer back
|
||||||
|
through an out parameter, one of them (`akbasic_environment_is_waiting_for` and its
|
||||||
|
sibling) as a public header change. Four more functions on the same call chains
|
||||||
|
(`scanner.c` `peek` and `match_next_char`, `sink_akgl.c` `putchar_at`, `echo_line`
|
||||||
|
and `edit_key`) had to move with them. **The wrapper shape was right and the
|
||||||
|
consumer's signatures were wrong**, which is not what the first count assumed.
|
||||||
|
|
||||||
|
The truncation six are a contract decision rather than an accident, and they are
|
||||||
|
what is genuinely left. The sharpest is `akbasic_runtime_error` — the one function
|
||||||
|
that tells the user *what went wrong*, formatting a 12,384-byte error message into
|
||||||
|
a 512-byte line. Truncating a report there is correct; raising `AKERR_OUTOFBOUNDS`
|
||||||
|
would replace the diagnosis with a second, different failure. Two more are
|
||||||
|
truncation-tolerant renderers that print what fits and stop, one of which reads
|
||||||
|
`snprintf`'s return value to *detect* the truncation and skip the rest of the
|
||||||
|
render. The remaining three read host-supplied strings into fixed fields.
|
||||||
|
|
||||||
|
Two of those three, in `akbasic/src/host.c`, are a latent defect the port surfaced
|
||||||
|
rather than a decision: a host-registered type name over 31 characters truncates
|
||||||
|
silently, and two names sharing a 31-character prefix then collide in
|
||||||
|
`akbasic_structtype_find` — where `structtype.c` refuses the identical case
|
||||||
|
outright with a limit message. The two registration paths disagree. That is
|
||||||
|
akbasic's to fix, and it is flagged at the site.
|
||||||
|
|
||||||
|
### What the recount found, and where it went
|
||||||
|
|
||||||
|
Every blocked site 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-six above and the only shape still blocking a conversion | #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 was 8 of the 13 sites the first count could not convert. **Ruled invalid** — the consumer changes its own signature, and now has | #38 |
|
||||||
|
|
||||||
|
**#38 is the one worth reading, because it is the one that was wrong.** It asked
|
||||||
|
this library to grow a form a `bool` predicate could call, and the answer was that
|
||||||
|
a predicate which cannot report an error should stop returning `bool`. Seven of its
|
||||||
|
eight sites converted on that basis, and they are why the count is 2.2% and not 4.1%.
|
||||||
|
The `ctype.h` half of the same filing is settled too: `isspace`, `isdigit`,
|
||||||
|
`isalnum` and `toupper` cannot fail, so there is nothing for a wrapper to return
|
||||||
|
and no reason to add one. What a caller does need is the `(unsigned char)` cast
|
||||||
|
every correct `ctype.h` call takes, and that is akbasic's note to keep, not this
|
||||||
|
library's.
|
||||||
|
|
||||||
|
The `bsearch` comparator has **no issue of its own**. #38 attributed it to akbasic
|
||||||
|
`#14`, which is a mis-citation — that issue is the `COLLISION`/`BUMP` pairing
|
||||||
|
threshold. Converting the comparator means dropping `bsearch(3)` for an in-house
|
||||||
|
binary search that can propagate, on a lookup that runs once per scanned
|
||||||
|
identifier, and that wants filing against akbasic before anybody does it.
|
||||||
|
|
||||||
|
**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 278 conversions that
|
||||||
|
produced less code than it started with.
|
||||||
|
|
||||||
|
**The port also found a defect in akbasic rather than in this library.** `DLOAD`
|
||||||
|
leaked a file descriptor: its read loop sat inside an `ATTEMPT` block and the
|
||||||
|
`PASS` in it returned past `CLEANUP`, so a scan error left the file open. Hoisting
|
||||||
|
the loop into its own helper — which converting `fgets` required anyway, because
|
||||||
|
neither `CATCH` nor `PASS` is legal in a loop inside an `ATTEMPT` — fixes it. That
|
||||||
|
is the protocol's own rule catching a real leak the moment somebody had to obey it.
|
||||||
|
|
||||||
|
### 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 313 calls it now
|
||||||
|
makes:
|
||||||
|
|
||||||
|
| Area | Calls | |
|
||||||
|
|---|---|---|
|
||||||
|
| Strings | 149 | 47.6% |
|
||||||
|
| Memory | 72 | 23.0% |
|
||||||
|
| Formatted output | 43 | 13.7% |
|
||||||
|
| Streams and files | 36 | 11.5% |
|
||||||
|
| String → number | 12 | 3.8% |
|
||||||
|
| Hashing | 1 | 0.3% |
|
||||||
|
| **Collections** | **0** | **0%** |
|
||||||
|
|
||||||
|
**Five sixths 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 2.2%; it is the 0%.** A recount that moves
|
||||||
|
92% to 2% 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.
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ this up. Rebuild against the new header.
|
|||||||
`AKSL_VERSION_CHECK()` catches the pairing at runtime if a stale `.so` ever does
|
`AKSL_VERSION_CHECK()` catches the pairing at runtime if a stale `.so` ever does
|
||||||
end up on the path.
|
end up on the path.
|
||||||
|
|
||||||
Everything below comes out of `TODO.md` sections 2.1 and 2.2 — the confirmed
|
Everything below was recorded in `TODO.md` before the move to the tracker — the confirmed
|
||||||
defects, all of which were reproduced against the 0.1.0 library before being
|
defects, all of which were reproduced against the 0.1.0 library before being
|
||||||
fixed.
|
fixed.
|
||||||
|
|
||||||
|
|||||||
@@ -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
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 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))
|
||||||
|
|
||||||
|
|||||||
@@ -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
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
|
* 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 {
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -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
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
|
* 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.
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
Reference in New Issue
Block a user