Skip to content

fix: repair the 10 test files that were failing for unrelated reasons - #196

Merged
JalonSolov merged 2 commits into
vlang:mainfrom
metif12:fix/failing-tests
Oct 4, 2026
Merged

JalonSolov merged 2 commits into
vlang:mainfrom
metif12:fix/failing-tests

Conversation

@metif12

@metif12 metif12 commented Oct 2, 2026

Copy link
Copy Markdown
Contributor

Follow-up to #195. That got the build and make testfmt green; this brings
make test from 37/47 to 46/47. The one remaining failure is shuf_test.v,
which is issue #189 and gets its own PR.

common/pwd: group ids came back as garbage

getgrouplist() and getgroups() fill an array of gid_t, which is 32 bits
wide, but the V declarations handed them a []int, i.e. i64. The kernel wrote
the low half of each slot and left the upper half holding the previous entry, so
gids arrived as 103079215108 — that is 24 * 2^32 + 4, i.e. adm's 4 with
cdrom's 24 sitting above it:

$ groups
GNU: mr adm cdrom sudo dip plugdev users docker
before: mr adm sudo plugdev mr root root root root

Both functions now fill a []u32.

getgrgid() was also declared as returning &C.passwd instead of &C.group.
That only ever worked because gr_name happens to sit at the same offset as
pw_name; gr_mem and gr_gid do not line up. There is now a proper struct.

groups and id now agree with GNU for the plain, named-user, -nG and -G
forms.

tail: KiB was decimal, and PB/EB used the binary multiplier

The suffix table mapped both kb and kib to 1000, both mb and mib to
1000^2, and so on — so tail -c 1KiB read a third of what it should have.
petabyte was also terra * kilo and exabyte peta * kilo, i.e. the binary
multipliers, instead of continuing the decimal chain. The two chains are now
separate and correct, and the test asserts GNU's actual values instead of
mirroring the constants back at itself.

The overflow check was result == 0 && number != 0, which cannot catch a
wrapped product; it now checks the division comes back out, so 20E is rejected
the way GNU rejects it. Z/Y/R/Q stay unsupported: although GNU's help mentions
them, GNU itself rejects every one with "Value too large for defined data type"
(checked against 9.4).

Verified end to end against GNU for 2b, k, K, KB, KiB, M, MB,
MiB, G, GB, GiB, T, TB, TiB, P, PB, PiB, E, EB, EiB,
plus the rejection cases.

numfmt: 2000.0 and 2e+06

f64.str() always keeps a fractional part and switches to scientific notation
above a million, so --to=none 2000000 printed 2e+06 and 2K printed
2000.0. Both go through a helper based on strlong() that drops a trailing
.0.

stat: the default Device: field

The default format used %Dh/%dd, printing the device number in hex over the
same number in decimal — 830h/2096d where GNU prints 8,48. GNU's default is
Device: %Hd,%Ld, and %H/%L are two character tokens that
scan_for_tokens() did not handle; there was already a TODO about it. Both are
implemented and the default format now matches.

Four tests were asserting the wrong thing, or crashing

  • tail_test and fmt_test captured &result for a local in setup() and
    returned the closure. Once setup() returned the pointer dangled and the
    closure appended to freed stack memory:

    malloc_uninit(0xd0a07408218d1b1f < 0)
    V panic: memory allocation failure
    

    The array header is now heap allocated. (cut_test and paste_test use the
    same shape but call the closure in scope, so they are fine.)

  • string_to_i64_test used ! on an option return, which does not compile.

  • sum_test had const util next to import io.util; the import is aliased.

  • truncate_test computed its expectations as 24 * 1000 * 1000 * 1000. V's
    int is 32-bit, so the expectation wrapped to -1769803776 while the
    value under test, a u64, was right all along. The expectations are now u64.

Testing

  • v run build.vsh — all 76 utilities build
  • make testfmt — passes
  • v test . — 46/47 files pass, up from 37/47

Verified on V 0.5.2 (0dc6a69, upstream vlang/v master), Ubuntu 24.04, GNU
coreutils 9.4.

main does not build with V 0.5.2, and `make testfmt` is red too, so every
CI job has been failing. This gets both gates back to green without changing
any intended behaviour.

Compile fixes, one per cause:

- `v.mathutil` is gone; `min`/`max` now live in `math`. Affects basenc, cut
  and tail.
- `for ; cond; post {}` and `for i := n; i; i-- {}` are no longer accepted:
  the condition has to be `bool`. Affects cksum and expand.
- `time.sleep` takes a `time.Duration`, not a number of seconds. sleep now
  converts explicitly and saturates instead of overflowing, so `sleep inf`
  keeps waiting rather than wrapping around to roughly zero.
- `const` cannot hold the result of a function call, so printenv's NUL
  terminator became a function.
- `setup_cp_command`/`setup_mv_command`/`setup_command` (head) were declared
  with `?`, but every failure path in them calls the @[noreturn]
  `common.exit_with_error_message`, so they never return an error and their
  callers had no `err` to look at. They now return plain tuples.
- `C.statvfs` is already declared privately by vlib/builtin/cfns.c.v, so a
  local declaration resolved to that one and failed to compile. stat binds a
  distinct name to the libc call with #define.
- `struct utmpx` was declared twice, in common/readutmp_nix.c.v and again in
  src/users/users.c.v with a different, shorter field list, which V now
  rejects as a redeclaration. Declared once, with the full layout.

stat additionally needed more than a compile fix:

- src/stat/stat_to_vlib.v (now src/stat/modes.v) imported v.scanner and
  v.pref to decode --printf escapes. Building it pulled the whole V compiler
  into the program: `v build` of src/stat never finished, and taking the
  machine down with it. The escapes are now decoded directly.
- `<bits/statx.h>` is a glibc internal header, so it is absent on musl based
  distributions (issue vlang#145). The kernel uapi `<linux/stat.h>` is used instead.
- `statx()` wrote the kernel's 224 byte struct through a `voidptr` into a
  V struct that was 160 bytes, so every call corrupted memory past the end of
  the allocation; stat segfaulted on every invocation. The C struct is now
  declared and used for real. `struct statvfs` has the same problem and its
  V mirror now matches the ABI including the trailing spare.
- `$embed_file` no longer resolves inside a function body, so fstypes.txt is
  embedded at module scope.

wc needed a fix of its own:

- `read_chunk` returned `?FileChunk`, but its caller inspected `err`, which
  does not exist for an option. os.File.read also reports end of file as an
  `os.Eof` error, and reading a zero-length chunk indexed `buffer[-1]`, so
  `wc` on an empty file would have panicked. It now returns `!FileChunk` and
  distinguishes exhaustion from a real read error.
- `\r` was matched both as "ignore" and as whitespace, which V rejects as a
  duplicate case.
- `byte(bool)` is no longer a valid conversion; the single-count check counts
  the selected options instead.

Finally, `v fmt -w .` over the 16 files that `v fmt -verify` rejected.

With this, `make` builds all 76 utilities and `make testfmt` passes.
Follow-up to vlang#195, which got the build and the formatting gate green. This
brings `make test` from 37/47 to 46/47. The only remaining failure is
shuf_test.v, which is issue vlang#189 and gets its own PR.

## common/pwd: group ids came back as garbage

getgrouplist() and getgroups() fill an array of gid_t, which is 32 bits wide,
but the V declarations handed them a []int, i.e. i64. The kernel wrote the low
half of each slot and left the upper half holding the previous entry, so gids
arrived as 103079215108 (24 * 2^32 + 4, i.e. adm's 4 with cdrom's 24 above it).
The two functions now fill a []u32.

getgrgid() was also declared as returning `&C.passwd` rather than
`&C.group`. That happened to work because gr_name sits at the same offset as
pw_name, but gr_mem/gr_gid do not line up, so there is now a proper struct.

groups and id now agree with GNU for the plain, named-user, -nG and -G forms.

## tail: KiB was decimal, and PB/EB used the binary multiplier

The suffix table mapped both `kb` and `kib` to 1000, both `mb` and `mib` to
1000^2, and so on, so `tail -c 1KiB` read a third of what it should have.
`petabyte` was also `terra * kilo` and `exabyte` `peta * kilo`, i.e. the binary
multipliers, instead of continuing the decimal chain. The two chains are now
separate and correct, and the test asserts GNU's actual values rather than
mirroring the constants.

The overflow check was `result == 0 && number != 0`, which cannot catch a
wrapped product; it now checks that the division comes back out. `20E` is
rejected like GNU rejects it. Z/Y/R/Q stay unsupported, because although
GNU's help mentions them, GNU itself rejects all of them with "Value too large
for defined data type" (checked against 9.4).

Verified end to end against GNU for 2b, k, K, KB, KiB, M, MB, MiB, G, GB, GiB,
T, TB, TiB, P, PB, PiB, E, EB and EiB, plus the rejection cases.

## numfmt: 2000.0 and 2e+06

f64.str() always keeps a fractional part and switches to scientific notation
above a million, so `--to=none 2000000` printed `2e+06` and `2K` printed
`2000.0`. Both now go through a helper based on strlong() that drops a trailing
`.0`.

## stat: the default Device: field

The default format used `%Dh/%dd`, which printed the device number in hex over
the same number in decimal: `830h/2096d` where GNU prints `8,48`. GNU's
default is `Device: %Hd,%Ld`, and %H/%L are two character tokens that
scan_for_tokens() did not handle; there was already a TODO about it. Both
tokens are implemented and the default format matches.

## Three tests were asserting the wrong thing, or crashing

* `tail_test` and `fmt_test` captured `&result` for a local in `setup()` and
  returned the closure. Once setup() returned, the pointer dangled and the
  closure appended to freed stack memory:
  `malloc_uninit(0xd0a07408218d1b1f < 0)`. The array header is now heap
  allocated. (cut_test and paste_test use the same shape but call the closure
  in scope, so they are fine.)
* `string_to_i64_test` used `!` on an option return, which does not compile.
* `sum_test` had `const util` next to `import io.util`. The import is aliased.
* `truncate_test` computed its expectations as `24 * 1000 * 1000 * 1000`. V's
  `int` is 32-bit, so the expectation wrapped to -1769803776 while the value
  under test, a u64, was right all along. The expected values are now u64.

## Testing

`v run build.vsh` builds all 76 utilities, `v fmt -verify .` passes, and
`v test .` reports 46 of 47 files passing, up from 37.
@JalonSolov
JalonSolov merged commit 475101a into vlang:main Oct 4, 2026
0 of 4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants