<div dir="ltr"><div dir="ltr"><br></div><br><div class="gmail_quote"><div dir="ltr" class="gmail_attr">On Fri, Aug 27, 2021 at 6:20 AM Rob Landley <<a href="mailto:rob@landley.net">rob@landley.net</a>> wrote:<br></div><blockquote class="gmail_quote" style="margin:0px 0px 0px 0.8ex;border-left:1px solid rgb(204,204,204);padding-left:1ex">On 8/26/21 5:56 PM, enh wrote:<br>
>     I keep telling people I could spend a focused year on JUST the test suite and<br>
>     they don't believe me. When people talk about function testing vs regression<br>
>     testing vs coverage testing I get confused because it's all the same thing? <br>
> <br>
> <br>
> i'll include the main failure modes of each, to preempt any "yes, but"s by<br>
> admitting that _of course_ you can write fortran in any language, but the idea<br>
> is something like:<br>
> <br>
> integration testing - answers "does my product work for real use cases?". you<br>
> definitely want this, for obvious reasons, and since your existing testing is<br>
> integration tests, i'll say no more. other than that the failure mode here is<br>
> relying only on integration tests and spending a lot more time/effort debugging<br>
> failures than you would if you could have caught the same issue with a unit test.<br>
<br>
I'm relying on the fact I wrote almost all the code myself, and thoroughly<br>
reviewed the rest, to be able to mentally model what everything is doing.<br>
<br>
That said, I'm trying to get the bus number up so you don't NEED me to do this<br>
sort of thing...<br></blockquote><div><br></div><div>exactly.</div><div><br></div><div>(though to be honest, i've found it useful for myself when everything's swapped out after a year or two. someone asked me about some code i'd written recently, and i told them they were wrong, so-and-so had written it, and they pulled out `git log` as proof.)</div><div> </div><blockquote class="gmail_quote" style="margin:0px 0px 0px 0.8ex;border-left:1px solid rgb(204,204,204);padding-left:1ex">
> unit testing - reduces the amount of digging you have to do _when_ your<br>
> integration tests fail. (also makes it easier to asan/tsan or whatever, though<br>
> this is much more of a problem on large systems than it is for something like<br>
> toybox, where everything's small and fast anyway, versus "30mins in to<br>
> transcoding this video, we crash" kinds of problem.) for something like toybox<br>
> you'd probably be more interested in the ability to mock stuff out --- your "one<br>
> day i'll have qemu with a known set of processes" idea,<br>
<br>
It's kinda hard to test things like ps/ifconfig/insmod outside of a carefully<br>
controlled known environment.<br></blockquote><div><br></div><div>that's what <a href="https://devopedia.org/mock-testing">https://devopedia.org/mock-testing</a> is for. (lets you easily test extreme and/or "shouldn't happen" values too.)</div><div> </div><blockquote class="gmail_quote" style="margin:0px 0px 0px 0.8ex;border-left:1px solid rgb(204,204,204);padding-left:1ex">
> but done by swapping<br>
> function pointers. one nice thing about unit tests is that they're very easily<br>
> parallelized. on a Xeon desktop i can run all several thousand bionic unit tests<br>
> in less than 2s... whereas obviously "boot a device" (more on the integration<br>
> test side) takes a lot longer. the main failure mode here (after "writing good<br>
> tests is at least as hard as writing good code", which i'm pretty sure you<br>
> already agree with, and might even be one of your _objections_ to unit tests),<br>
<br>
Eh, the toys/example/demo_$THINGY commands are sort of intended to do this kind<br>
of thing for chunks of shared infrastructure (library code, etc).<br>
<br>
My objection here is really granularity, if you test at TOO detailed a level<br>
you're just saying "this code can't change". I recently changed xabspath() to<br>
have a flag based interface, changed its existing users in the commands,  and<br>
have the start of a toys/example/demo_abspath.c (which I mentioned in my blog I<br>
was too exhausted to properly finish at the time). Granular tests directly<br>
calling the functions would have been invalidated by the change, meaning I'd<br>
either have deleted them or rewritten them.<br>
<br>
With other people's test suites I often encounter test failures that don't MEAN<br>
anything. Some test is failing because the semantics of something somewhere<br>
changed, and none of the users care, and the test suite accumulates "known<br>
failures" like code emitting known warnings.<br>
<br>
A libc has an API with a lot of stable documented entry points. Toybox's entry<br>
points are almost entirely command line utilities with a shared entry codepath<br>
(including option parsing) and a shared library of common functions. </blockquote><div><br></div><div>well, this is the problem (and for mocking especially): for unit testing to work any better than integration testing, you have to design for testing. for example: if your "ps" needs to be able to take real data from the system or synthetic data from a mock, you need to have split the code that way. and that's a lot easier if you start with that in mind rather than try to retrofit it.</div><div> </div><blockquote class="gmail_quote" style="margin:0px 0px 0px 0.8ex;border-left:1px solid rgb(204,204,204);padding-left:1ex">I don't<br>
want to test the lib/*.c code directly from something that ISN'T a command (some<br>
other main() in its own .c function, possibly accessing it via dlopen() or<br>
something) because the top level main.c initializes toy_list[] and has<br>
toy_init() and toy_find() and toy_exec() and so on. If I factored that out I'd<br>
_only_ be doing so for the test suite, not because it made design sense. I don't<br>
want to duplicate plumbing and test in a different environment than I'm running in.<br>
<br>
The mkroot images are "tiny but valid". It's a theoretically real system you<br>
could build up from, and tells me "how does this behave under musl on a bunch of<br>
targets", using a real Linux kernel and so on.<br>
<br>
> is writing over-specific unit tests. rather than writing tests to cover "what<br>
> _must_ this do to be correct?" people cover "what does this specific<br>
> implementation happen to do right now, including accidental implementation<br>
> details?".<br>
<br>
Yup. Seen a lot of that. :(<br>
<br>
> (i've personally removed thousands of lines of misguided tests that<br>
> checked things like "if i pass _two_ invalid parameters to this function, which<br>
> one does it report the error about?", where the correct answer is either "both"<br>
> or "who cares?", but never "one specific one".)<br>
<br>
I've bumped into some of that in toysh because I want to match bash's behavior<br>
and alas bash is one of those "the implementation is currently the standard"<br>
things where every implementation detail hiccup IS the current spec.<br>
<br>
That said, I've blogged about making a few digressions anyway just because my<br>
plumbing doesn't work like bash's does (they're gratuitously making multiple<br>
passes over the data and I'm doing it all in one pass, and there are some places<br>
where "all x happens before all y" bubbles visibly to the surface and I just<br>
went no.) For example the "order of operations" issue in<br>
<a href="https://landley.net/notes-2021.html#18-03-2021" rel="noreferrer" target="_blank">https://landley.net/notes-2021.html#18-03-2021</a><br>
<br>
> coverage - tells you where your arse is hanging out the window _before_ your<br>
> users notice. (i've had personal experiences of tests i've written and that two<br>
> other googlers have code reviewed that -- when i finally got the coverage data<br>
> -- turned out to be missing important stuff that [i thought] i'd explicitly<br>
> written tests for.<br>
<br>
This is what I mean by testing the error paths. If I have a statement the code<br>
flow doesn't ever go through in testing, I'd like to know why. There's<br>
presumably tools for this (I think valgrind has something), but that's waaaaaay<br>
down the road.<br></blockquote><div><br></div><div>the regular llvm coverage stuff works fine. i sent you a script and the results a while back, but i don't remember whether i offered to add a `make coverage` option? (at some point i should get it "for free" from Android's CI, but not yet.)</div><div> </div><blockquote class="gmail_quote" style="margin:0px 0px 0px 0.8ex;border-left:1px solid rgb(204,204,204);padding-left:1ex">
> Android's still working on "real time" coverage data showing<br>
> up in code reviews, but "real Google" has been there for years, and you'd be<br>
> surprised how many times your tests don't test what you thought they did.)<br>
<br>
Sadly, I would not by surprised. :(<br>
<br>
> the main failure mode i've seen here is that you have to coach people that "90% is<br>
> great", and that very often chasing the last few percent is not a good use of<br>
> time,<br>
<br>
<a href="https://en.wikipedia.org/wiki/Pareto_principle" rel="noreferrer" target="_blank">https://en.wikipedia.org/wiki/Pareto_principle</a><br>
<br>
And here is an excellent mathematical walkthrough of the math behind it:<br>
<br>
  <a href="https://www.youtube.com/watch?v=sPQViNNOAkw#t=6m43s" rel="noreferrer" target="_blank">https://www.youtube.com/watch?v=sPQViNNOAkw#t=6m43s</a><br>
<br>
Which is why the old saying "the fist 90% of the work takes 90% of the time, the<br>
remaining 10% of the work takes the other 90% of the time" is ALMOST right, it's<br>
that the next 9% takes another 90% in a xeno's paradox manner (addressing 90% of<br>
what's left takes a constant amount of time) until you shoot the engineers and<br>
go into production.<br>
<br>
> and in the extreme can make code worse. ("design for testability" is good,<br>
> but -- like all things -- you can take it too far.)<br>
<br>
My grumble is I'm trying to write a lot of tests that toybox and the debian host<br>
utilities can BOTH pass. I want to test the same code linked against glibc,<br>
musl, and bionic. I want to test it on big endian and little endian, 32 bit and<br>
64 bit, systems that throw unaligned access faults, nommu...<br></blockquote><div><br></div><div>yeah, that's an orthogonal problem. (and why i really don't want to lose 32-bit from CI, because i certainly don't use 32-bit [unless you count Raspberry Pi]!)</div><div> </div><blockquote class="gmail_quote" style="margin:0px 0px 0px 0.8ex;border-left:1px solid rgb(204,204,204);padding-left:1ex">
>     You<br>
>     have to test every decision point (including the error paths), you have to<br>
>     exercise every codepath (or why have that codepath?) and you have to KEEP doing<br>
>     it because every distro upgrade is going to break something.<br>
> <br>
> yeah, which is why you want all this stuff running in CI, on all the platforms<br>
> you care about.<br>
<br>
People use "continuous integration" as an excuse not to have releases. </blockquote><div><br></div><div>some people might do that, but that's orthogonal. "continuous integration" is literally just "we run the tests on every checkin". whether you use that for good or evil is orthogonal.</div><div> </div><blockquote class="gmail_quote" style="margin:0px 0px 0px 0.8ex;border-left:1px solid rgb(204,204,204);padding-left:1ex">No two<br>
people should ever run quite the same version and see quite the same behavior,<br>
we're sure random git snapshot du jour is fine...<br>
<br>
I object on principle.<br>
<br>
>     In my private emails somebody is trying to make the last aboriginal linux<br>
>     release work and the old busybox isn't building anymore because makedev() used<br>
>     to be in #include <sys/types.h> and now it's moved to <sys/sysmacros.h>. (Why? I<br>
>     dunno. Third base.) <br>
> <br>
> the pain of dealing with that pointless deckchair crap with every glibc update<br>
> is one reason why (a) i've vowed never to do that kind of thing again in bionic<br>
> [we were guilty of the same crime in the past, even me personally; the most<br>
> common example being transitive includes] and (b) i'm hoping musl will care a<br>
> bit more about not breaking source compatibility ... but realize he's a bit<br>
> screwed because code expecting glibc might come to rely on the assumption that<br>
> <sys/types.h> *doesn't* contain makedev(), say --- i've had to deal with that<br>
> kind of mess myself too. sometimes you can't win.<br>
<br>
He has a very active mailing list and IRC channel (now on <a href="http://libre.net" rel="noreferrer" target="_blank">libre.net</a> like<br>
everybody else) where they argue about that sort of thing ALL THE TIME. (That<br>
said, I poked him to see if he wants to make a policy statement about this. Or<br>
has one somewhere already.)<br>
<br>
My complaint way back when was objecting to the need to #define<br>
GNU_GNU_ALL_HAIL_STALLMAN in order to get the definition for linux syscall<br>
wrappers (which have NOTHING to do with the gnu project). I made puppy eyes at<br>
Rich until he added the _ALL_SOURCE define so musl headers could just give me<br>
everything they knew how to do do without micromanaging feature macros. (I'm<br>
already #including some headers and not including others, that's the granularity<br>
that makes SENSE...)<br></blockquote><div><br></div><div>yeah, given that historical accident means that you effectively can't _not_ have _BSD_SOURCE on Android, and that there's a lot of _GNU_SOURCE (but not all) that was also always "on by default", i've been leaning towards "you get all the things, all the time". it's a lot less confusing to n00bs, in particular. and there's more than one of them born every minute.</div><div> </div><blockquote class="gmail_quote" style="margin:0px 0px 0px 0.8ex;border-left:1px solid rgb(204,204,204);padding-left:1ex">
And then I wound up doing:<br>
<br>
  #define unshare(flags) syscall(SYS_unshare, flags)<br>
  #define setns(fd, nstype) syscall(SYS_setns, fd, nstype)<br>
<br>
anyway. :)<br>
<br>
Rob<br>
</blockquote></div></div>